Skip to main content

Sharing data between addons

Nothing crosses a realm on its own. An addon's documents, config values and in-memory state are its own; what a peer can see is exactly what the addon put on one of three channels, and each channel carries one kind of thing.

The three channels​

ChannelCarriesKept whereWho writesPeer reads it
core.shareda value every realm should have nowevery realm's mirrorthe ownersynchronously, from its mirror
core.eventsa happeningnowhere — delivered and forgottenthe ownera listener, the same tick
core.rpcan answer to a questionwith the ownerthe owner's handlera promise, next tick

Pick by what the peer needs to do with it:

  • The peer needs the latest value, cheaply and often — a shop's open flag, the current event, a price. Put it on a shared key. One publish, every realm reads it locally for as long as it wants.
  • The peer needs to react when something happens — a purchase, a level-up, a reset. Emit an event. It carries a payload, costs one message, and is not kept: a peer that was not listening has missed it.
  • The peer needs something specific, or wants to change something — one player's balance, a write to a document. Serve an RPC method. The owner answers with its own data, authorized against the acting player.

A value only some peers want, some of the time, is an RPC answer, not a shared key — the mirror is paid for by every realm on every write.

What stays local​

core.db is local. A collection is this addon's; nothing in it is served, announced or mirrored by default. The same holds for config: its schema and values stay with the addon that declares them.

That is deliberate. Storage and exposure are different decisions, and an addon that persists a document has not said anything about who may read it.

Mapping a document onto a channel​

An owner exposes a document by naming the channel, in one line each:

const settings = core.db.collection('settings', { schema: schema<Settings>({ defaults }), accept: worldTarget() });

const { shared, events } = core.register({
manifest,
shared: { event: defaults.event },
events: { purchase: event<{ playerId: string; gold: number }>() },
});

// A shared key: every realm has the current value. Fires on load too, so this is right at boot.
settings.for(world).subscribe(doc => shared.event.set(doc.event));

// An event: peers hear it happened.
core.rpc.serve<EconomyApi>({
deductGold: ({ playerId, gold, actorId }) => {
authorize({ entity: playerId }, actorId, 'write');

const doc = balances.for(playerOf(playerId));

doc.patch({ gold: (doc.get()?.gold ?? 0) - gold });
events.purchase.emit({ playerId, gold });

return doc.get();
},
});

A config setting is shared the same way: the owner mirrors it on a shared key, emits an event when it changes, or serves it.

Reading from the other side​

const economy = core.shared.of<EconomyShared>('drav0011_economy'); // undefined until it announces
const purchases = core.events.of<EconomyEvents>('drav0011_economy'); // always a tree
const api = core.rpc.typed<EconomyApi>('drav0011_economy');

economy?.event.subscribe(event => hud.setEvent(event));
purchases.purchase.subscribe(({ playerId, gold }) => hud.flash(playerId, `-${gold}`));

const balance = await api.balance({ playerId: player.id, actorId: player.id });

Each read is typed by what the owner exports — typeof sharedDef, typeof eventsDef, the API interface — from a types package the peer installs. Nothing about the shape travels at runtime beyond the shared key names.

Where each value can be seen​

PlaceOwnerSurvives restartPeers see it by defaultAuthoritative
an observablethis addonnonoyes, for what it holds
a db documentthis addonyesnoyes
the config accessor treethis addonyesnoyes
a shared key, own namespacethis addonno — map a document onto ityes, from their mirroryes
a shared key, a peer's namespacethe peerno—no: a copy; writes are dropped
an eventthe sendernoonce, if listening—
core.node.statethe transportnoyesthe raw mirror, framework keys included

Next steps​

  • core.shared — the mirror as typed trees
  • core.events — broadcasts delivered and forgotten
  • RPC — request and reply, with serve<Api> and typed<Api>
  • Trust model — what the channels do and do not defend against