core.registry
core.registry is a live directory of every bedrock-core addon present in the world — the local addon plus every peer discovery can currently see. Each peer's announce meta blob is interpreted as an AddonManifest, keyed by its namespace.
The directory itself is addons, a ReadonlyObservable list derived from discovery. all(), get() and has() all read that one list, so there is never a cached answer to "who is here" that can disagree with the live one.
Import
import { core } from '@bedrock-core/server';
import type { RegisteredAddon, AddonListener, CollisionListener, IncompatibleListener, IncompatiblePeer } from '@bedrock-core/server';
Usage
core.registry.addons.subscribe(addons => redraw(addons)); // who is here, as a value
core.registry.all(); // readonly RegisteredAddon[] — self + all live peers
core.registry.get('drav0011_economy'); // by namespace, or undefined
core.registry.has('drav0011_economy'); // boolean
core.registry.onRegister(addon => console.warn('joined:', addon.id));
core.registry.onUnregister(addon => console.warn('left:', addon.id));
core.registry.onNamespaceCollision(info => console.error('collision on', info.id));
RegisteredAddon
type RegisteredAddon = AddonManifest & {
id: string;
self: boolean;
runtimeVersion: string;
};
Every entry carries the full manifest plus three fields the registry adds:
| Field | Meaning |
|---|---|
id | The namespace, creator_pack. The key everything else uses. |
self | true for the local addon, false for a peer. |
runtimeVersion | The @bedrock-core/server-runtime version that addon was built against — not its own version. This is what core.host compares. |
A peer's manifest is reconstructed from its discovery meta. A node that published no meta keeps its whole namespace as pack and an empty string as creator — the id is never split back into halves. runtimeVersion defaults to 0.0.0 when absent or malformed, so that peer loses an election rather than corrupting it.
API
addons
core.registry.addons: ReadonlyObservable<readonly RegisteredAddon[]>
Every registered addon as an observable list, the local one first. Subscribe to it to track who is present; computed() over it to derive something from the set.
const addonCount = computed(() => core.registry.addons.get().length, [core.registry.addons]);
It republishes when an addon appears or disappears, and when a peer re-announces something new — never on the heartbeats that repeat what a peer already said.
all
core.registry.all(): readonly RegisteredAddon[]
A snapshot of addons: the local addon first, then every live peer.
for (const addon of core.registry.all()) {
console.warn(`${addon.id} v${addon.version}${addon.self ? ' (me)' : ''}`);
}
get
core.registry.get(id: string): RegisteredAddon | undefined
Look up an addon by its namespace. Returns the local addon when id === core.id.
has
core.registry.has(id: string): boolean
Whether an addon with that namespace is present. This is the predicate feature conditions are usually built from — see FeatureManager.
onRegister
core.registry.onRegister(listener: AddonListener): Unsubscribe
Fires when a peer becomes visible. Returns an unsubscribe function. This is a delta: to react to who is present rather than to each arrival, subscribe to addons.
const off = core.registry.onRegister((addon) => {
if (addon.id === 'drav0011_economy') { startTrading(); }
});
// later
off();
onRegister only fires for peers that appear after you subscribe. Peers already present are in all(). Since addons load in undefined order, code that must react to a peer either way should check has()/all() first and then subscribe — or use a feature, which evaluates its condition immediately and on every change.
onUnregister
core.registry.onUnregister(listener: AddonListener): Unsubscribe
Fires when a peer goes away — that is, when discovery evicts it after its TTL elapses without a heartbeat.
onNamespaceCollision
core.registry.onNamespaceCollision(listener: CollisionListener): Unsubscribe
interface CollisionInfo {
id: string;
instanceId: string;
}
Fires when another instance is announcing this addon's namespace — two packs both registered creator + pack with identical halves. The runtime also logs:
[bedrock-core] collision: another instance shares identity '<id>'
A collision is not recoverable at runtime; both addons are now sharing a transport address, a state namespace and a command namespace. Treat it as a packaging bug and change one of the two pack ids.
core.registry.onNamespaceCollision((info) => {
console.error(`[economy] another pack is using '${info.id}' (instance ${info.instanceId})`);
});
incompatible
core.registry.incompatible(): readonly IncompatiblePeer[]
interface IncompatiblePeer {
id: string;
pmin: number;
pmax: number;
}
Addons heard on the bus that this build cannot talk to, because the protocol ranges the two were built with do not overlap. They are present in the world but absent from all(): without a conversation there is no manifest to read.
They are listed rather than ignored because the alternative is worse than an error. An addon that cannot be reached, silently left out, looks exactly like an addon that was never installed — the catalog is confidently wrong instead of visibly incomplete.
onIncompatible
core.registry.onIncompatible(listener: IncompatibleListener): Unsubscribe
Fires the first time such an addon is heard. The runtime also logs:
[bedrock-core] '<id>' speaks sync protocol <pmin>-<pmax>, this addon speaks <min>-<max>; the two cannot talk. Update whichever is older.
The fix is always to update one of the two packs; there is no runtime setting that bridges the gap.
Dependencies
Dependencies are declared and matched by namespace, and they are soft: a missing one is reported but never blocks the addon from loading or running.
core.register({
manifest: {
creator: 'drav0011',
pack: 'shop',
packName: 'Shop',
version: '1.0.0',
dependencies: ['drav0011_economy'],
},
});
missing
core.registry.missing: ReadonlyObservable<readonly string[]>
The declared dependencies that are not currently present, as an observable list. Empty means satisfied.
missingDependencies
core.registry.missingDependencies(): readonly string[]
A snapshot of missing.
core.registry.missingDependencies(); // ['drav0011_economy'] until it registers
onDependenciesSatisfied
core.registry.onDependenciesSatisfied(listener: () => void): Unsubscribe
Fires when every declared dependency is present.
- If they are already satisfied when you subscribe — including the common case of no dependencies declared at all — the listener fires immediately, synchronously, inside the
onDependenciesSatisfiedcall. - After that it is edge-triggered: it fires once on each unsatisfied → satisfied transition, not on every peer that joins.
core.registry.onDependenciesSatisfied(() => {
const economy = core.registry.get('drav0011_economy');
if (!economy) { return; }
const rpc = core.rpc.typed<EconomyRPC>(economy.id);
rpc.getBalance({ player: 'Steve' })
.catch((error: unknown) => console.warn(`[shop] ${String(error)}`));
});
If a dependency later disappears the transition flips back, and a subsequent recovery fires your listener again. Write the callback so it is safe to run more than once.
Logging
The registry writes to console.info when the dependency picture changes:
[bedrock-core] 'drav0011_shop' missing dependencies: drav0011_economy
[bedrock-core] 'drav0011_shop' dependencies resolved: drav0011_economy
The first is also emitted once at startup if anything is missing then. Collisions use console.error; nothing here throws.