Event Registry
An in-page event bus exposed as window.__NF_REGISTRY__.
Resolves the orchestrator's ready promise retroactively and gives
micro frontends a typed channel for cross-MFE communication.
The orchestrator ships a small event bus that lives on
window.__NF_REGISTRY__. It is what
init-registry.mjs boots up before the orchestrator
runtime arrives, and it covers two jobs:
-
Resolving init promises retroactively. A subscriber
that arrives after the orchestrator has finished initializing still
receives
orch.init-ready— no race condition window like plainwindow.addEventListenerevents. - Robust cross-MFE communication. Micro frontends can publish and subscribe to typed streams without holding direct references to each other.
Opt-in feature — it does not exist until you load it.
window.__NF_REGISTRY__ is only created
when the registry has been initialized. For a quickstart host that
means adding the init-registry.mjs script (see
Installation below) before any consumer
code runs. For a custom orchestrator host it means calling
createRegistry({ ... }) from
@softarc/native-federation-orchestrator/registry and
assigning the result to window.__NF_REGISTRY__
yourself. If you skip both, the quickstart silently falls back to
the legacy mfe-loader-available custom event and
consumer code that calls
window.__NF_REGISTRY__.onReady(...) will throw
Cannot read properties of undefined.
The registry distinguishes two concepts:
-
Resources — fire-and-latch values, delivered
through
register/onReady. Once registered, every futureonReadyconsumer fires immediately. Use these for one-shot readiness signals (the orchestrator itself uses this fororch.init-ready). -
Event streams — append-only buffers, delivered
through
emit/update/on. Streams are bounded bymaxEventsand (optionally)maxStreams, and subscribers get configurable history replay.
Installation
There are two supported ways to make
window.__NF_REGISTRY__ available; pick the one that
matches how you ship the orchestrator.
Option A — quickstart hosts: load init-registry.mjs
Include init-registry.mjs before the
rest of your scripts (including the orchestrator's
quickstart.mjs and any consumer code). Optionally tune
the bus via data-* attributes on the script tag:
<script
src="https://unpkg.com/@softarc/native-federation-orchestrator@4.2.1/init-registry.mjs"
data-max-streams="50"
data-max-events="50"
data-remove-percentage="50"
></script>
After this script runs, window.__NF_REGISTRY__ is a
frozen NFEventRegistry instance. The orchestrator's
quickstart bundle detects it at the end of init and publishes
orch.init-ready on it (see
What the orchestrator publishes).
Option B — custom orchestrator hosts: create it yourself
If you bundle your own orchestrator script (calling
initFederation directly), init-registry.mjs
is the wrong shape — you don't load random
unpkg scripts in a bundled host. Instantiate the registry
from the /registry subpath and assign it to
window yourself, before
initFederation resolves:
import { createRegistry } from '@softarc/native-federation-orchestrator/registry';
import { initFederation } from '@softarc/native-federation-orchestrator';
// Same options as init-registry.mjs accepts via data-* attributes.
// removePercentage is a fraction here (0.5), not a percent (50).
window.__NF_REGISTRY__ = Object.freeze(
createRegistry({ maxEvents: 50, maxStreams: 50, removePercentage: 0.5 })()
);
initFederation(manifest, options).then(loaders => {
window.__NF_REGISTRY__.register('orch.init-ready', { ...loaders });
});
If neither option runs, the registry doesn't exist.
window.__NF_REGISTRY__ is undefined and
the orchestrator silently falls back to the legacy
mfe-loader-available CustomEvent. Consumer
code that calls window.__NF_REGISTRY__.onReady(...)
will throw — guard with
window.__NF_REGISTRY__?.onReady(...) if both wirings
must work side by side.
Configuration
| Attribute | Default (no attribute set) | Effect |
|---|---|---|
data-max-streams |
unbounded (no LRU eviction) | Maximum number of distinct stream types the registry will retain. When exceeded, the least-recently-emitted stream (and its history) is evicted. |
data-max-events |
1 (latest-only) |
Per-stream history depth. When exceeded, the oldest events are dropped — see Trimming below. |
data-remove-percentage |
1 event removed per trim |
When trimming kicks in, this percentage of
maxEvents is evicted in one batch (so subsequent
emits don't slice on every call). Expressed in %.
|
Without data-max-events, the registry only keeps the
most recent event per stream — fine for state-channels
(BehaviorSubject-style) but probably too aggressive for log-style
streams. Set data-max-events explicitly when you want
history.
If you instantiate the registry yourself via
createRegistry({ ... }) rather than the script, the
equivalent options are maxStreams,
maxEvents, and removePercentage.
removePercentage is a fraction here, not a
percent
— pass 0.5, not 50. The script tag divides
the data-attribute value by 100 before forwarding it.
Trimming
The trim policy is "batch eviction." When a stream's history exceeds
maxEvents, the registry drops
min(maxEvents - 1, ceil(maxEvents * removePercentage)) of
the oldest events at once, leaving the most recent
maxEvents - removed events. This avoids slicing on every
emit at the cost of a brief overshoot.
At least one event is always retained — even with
removePercentage: 1.0 or maxEvents: 1, the
most recent event stays available for new subscribers.
API
The full type is NFEventRegistry:
type NFEventRegistry = {
// Resources (one-shot readiness)
register<T>(type: string, resource: T | (() => Promise<T> | T)): Promise<void>;
onReady<T>(type: string, callback: (value: T) => void): NFEventUnsubscribe;
// Event streams
emit<T>(type: string, data: T): void;
update<T>(type: string, reducer: (current: T | undefined) => T): void;
on<T>(
type: string,
callback: (event: { data: T; timestamp: number }) => void,
opts?: { replay?: number }
): NFEventUnsubscribe;
// Maintenance
clear(type?: string): void;
};
Resources
register(type, resource)
Stores resource under type. If
resource is a function, it is invoked (and awaited) and
the result is stored. Any consumers waiting via
onReady(type, …) are notified.
__NF_REGISTRY__.register('app-config', async () =>
fetch('/config.json').then(r => r.json())
);
onReady(type, callback)
If type is already registered, callback
fires synchronously with the value. Otherwise it is
queued and fires the moment register(type, …) runs.
__NF_REGISTRY__.onReady('orch.init-ready', ({ loadRemoteModule }) => {
loadRemoteModule('team/mfe1', './Button');
});
Returns an unsubscribe function. When type was already
registered at subscribe time, the returned function is a no-op (the
callback has already fired).
Event streams
emit(type, data)
Publishes an event on type. The event is wrapped with a
timestamp, appended to the stream's history, and
delivered synchronously to every current subscriber.
__NF_REGISTRY__.emit('cart.changed', { itemCount: 3 });
update(type, reducer)
Publishes a new event whose value is derived from the previous one — useful when the next state depends on the last (counters, accumulating buffers, toggles).
The reducer receives a structured clone of the last
value (or undefined if the stream is empty), so it cannot
accidentally mutate stored history.
__NF_REGISTRY__.update('cart.itemCount', current => (current ?? 0) + 1);
Values passed through update must be
structured-clone-able when the stream is non-empty (no functions,
DOM nodes, or class instances).
on(type, callback, opts?)
Subscribes callback to type for every future
event. On subscribe, the most recent opts.replay events
from history are delivered via a microtask (so the unsubscribe handle
is returned synchronously, and the replay fires after the current task
completes).
opts.replay |
Behavior |
|---|---|
1 (default) |
Deliver only the most recent event — matches the "BehaviorSubject" pattern, where a fresh subscriber always sees the current state. Right default for state-channels. |
0 |
Suppress replay entirely. The subscriber only sees events emitted after it subscribes — right for one-shot signals where past history is irrelevant. |
N |
Deliver up to the last N events in chronological
order. Capped implicitly by maxEvents. Right for
late-joining consumers that need recent context (e.g. a debug
panel that wants the last 10 navigation events).
|
Replay payloads are structured-cloned, so mutating them in the callback never affects what later subscribers will see.
// Default: get the latest cart state immediately, then every future change.
__NF_REGISTRY__.on('cart.changed', ({ data, timestamp }) => render(data));
// Subscribe without backfill.
__NF_REGISTRY__.on('user.clicked', ({ data }) => track(data), { replay: 0 });
// Backfill the last 10 navigation events.
__NF_REGISTRY__.on('nav.route', ({ data }) => log(data), { replay: 10 });
Returns an unsubscribe function that removes the listener.
Maintenance
clear(type?)
With a type, drops the stream's history, listeners,
registered resource, and pending onReady callbacks for
that key. Without a type, wipes the entire registry.
Useful in tests; rarely needed in production.
What the orchestrator publishes
When the orchestrator's quickstart bundle finishes initializing, it
calls register('orch.init-ready', { … }) with the entire
NativeFederationResult payload —
loadRemoteModule, load, as,
config, adapters,
initRemoteEntry. Subscribers can destructure whichever
pieces they need:
__NF_REGISTRY__.onReady('orch.init-ready', ({ loadRemoteModule, initRemoteEntry }) => {
loadRemoteModule('team/mfe1', './Button');
});
For a fully custom orchestrator script (you call
initFederation yourself), wire the registry the same way
the quickstart does:
import { initFederation } from '@softarc/native-federation-orchestrator';
initFederation(manifest, options).then(loaders => {
window.__NF_REGISTRY__?.register('orch.init-ready', { ...loaders });
});
Patterns
State channel (BehaviorSubject-style)
// Producer
__NF_REGISTRY__.update('cart.itemCount', n => (n ?? 0) + 1);
// Consumer — gets the current count on subscribe + every change after
__NF_REGISTRY__.on('cart.itemCount', ({ data }) => {
document.querySelector('#cart-badge').textContent = data;
});
Event log (multiple subscribers, no backfill)
// Producer
__NF_REGISTRY__.emit('analytics.click', { id: 'cta-1' });
// Consumer — only sees events after subscribe
__NF_REGISTRY__.on('analytics.click', ({ data }) => track(data), { replay: 0 });
Late-joining diagnostic (bounded backfill)
__NF_REGISTRY__.on(
'orch.module-loaded',
({ data, timestamp }) => debugPanel.append({ ...data, at: timestamp }),
{ replay: 50 }
);
See also
- The orchestrator event-registry docs — the upstream version of this page.
- Getting Started — Avoiding race conditions — how the registry plugs into the HTML quickstart.
-
Configuration — every option on
initFederation.