Event type registration
This page explains how stored events are turned back into Dart objects, what libspiffy registers for you, and what you register yourself.
Why events must be registered
Section titled “Why events must be registered”libspiffy keeps every wallet, invoice and payment channel change as an event in an Eventador event store.
Each event is stored as CBOR together with its type name. When the store is read back (an aggregate
recovering after a restart, a projection catching up) Eventador looks the type name up in its
EventRegistry to find the factory that rebuilds the event. A type name with no factory fails with:
ArgumentError: Event type XYZ not registeredThat error means a stored event’s type was never registered in this process. The usual causes are an
event class of your own that you did not register, a type name that changed, or a test that called
EventRegistry.clear() and did not register libspiffy’s types again.
What libspiffy registers
Section titled “What libspiffy registers”LibSpiffyActorSystem.initialize() registers every event type libspiffy writes to its journal before it
starts the projections. It calls the static method LibSpiffyActorSystem.registerEventTypes(), which
covers:
- wallet events (
BitcoinWalletAggregate): wallet created, deleted and reconfigured, addresses, UTXOs and their reservations, transactions, deferred payments; - invoice events (
InvoiceAggregate): created, paid, expired, cancelled; - payment channel events (
PaymentChannelAggregate): requested, accepted, funded, opened, payments, closing, closed, expired, refunds.
It also registers a few types nothing emits any more (for example UTXOReservationPlacedEvent and
InvoiceStatusChangedEvent), because older journals contain them and a journal is never rewritten.
registerEventTypes() is idempotent. Call it yourself when you read a libspiffy journal without starting
the actor system: in a test, a migration tool, or a custom event store.
import 'package:libspiffy/libspiffy.dart';
void main() { LibSpiffyActorSystem.registerEventTypes(); // Now an EventStore opened on a libspiffy journal can be read.}Stable type names
Section titled “Stable type names”Every libspiffy event class declares a stableTypeName constant and returns it from typeName. That is
the name stored with each event, for example:
| Event class | Stored type name |
|---|---|
WalletCreatedEvent |
wallet.created |
AddressGeneratedEvent |
wallet.address.generated |
UTXOReceivedEvent |
wallet.utxo.received |
UTXOSpentEvent |
wallet.utxo.spent |
TransactionRecordedEvent |
wallet.transaction.recorded |
TransactionSpendDeferredEvent |
wallet.transaction.spend_deferred |
InvoiceCreatedEvent |
invoice.created |
InvoicePaidEvent |
invoice.paid |
ChannelOpenedEvent |
channel.opened |
ChannelClosedEvent |
channel.closed |
These are the domain events from package:libspiffy/internals.dart, not the coordinator events of the same
class name that arrive on coordinatorEvents. Coordinator events are never stored, so they need no
registration.
A stable name does not depend on the Dart class name. The journal keeps loading after a class is renamed,
and in an app built with --obfuscate, where class names are scrambled. Journals written by earlier
releases stored the class name (UTXOReceivedEvent); registerEventTypes() registers each class name as
an alias of the stable name, so those rows still load.
Registering your own events
Section titled “Registering your own events”You need this only if your own code writes events to an Eventador store, for example a custom aggregate
next to libspiffy’s (see Custom commands & events). Add eventador to your
dependencies, give the event a stable type name, and register it before anything reads the store, which in
practice means before initialize().
import 'package:eventador/eventador.dart';
class LoyaltyPointsAwarded extends Event { /// Stored with every event. Never change it. static const String stableTypeName = 'myapp.loyalty.points_awarded';
final String walletId; final int points;
LoyaltyPointsAwarded({ required this.walletId, required this.points, super.eventId, super.timestamp, super.version, super.metadata, });
@override String get typeName => stableTypeName;
@override Map<String, dynamic> toMap() => { ...super.toMap(), 'walletId': walletId, 'points': points, };
factory LoyaltyPointsAwarded.fromMap(Map<String, dynamic> map) => LoyaltyPointsAwarded( walletId: map['walletId'] as String, points: map['points'] as int, eventId: map['eventId'] as String?, timestamp: DateTime.parse(map['timestamp'] as String), version: map['version'] as int?, metadata: Map<String, dynamic>.from(map['metadata'] as Map? ?? {}), );}import 'package:eventador/eventador.dart';import 'package:libspiffy/libspiffy.dart';
import 'loyalty_events.dart';
Future<void> main() async { EventRegistry.register<LoyaltyPointsAwarded>( LoyaltyPointsAwarded.stableTypeName, LoyaltyPointsAwarded.fromMap, );
final libspiffy = LibSpiffyActorSystem(); await libspiffy.initialize(networkType: 'test', enableP2P: false);}Follow the same rules libspiffy does:
- Pick a namespaced name (
myapp.…) that cannot collide with libspiffy’swallet.,invoice.andchannel.names. - Never change a stable name once events carrying it are stored.
- If you rename a stored name anyway, register the old one as an alias:
EventRegistry.register<T>(newName, fromMap, aliases: const ['OldName']), orEventRegistry.registerAlias('OldName', newName).
To check what is registered, EventRegistry.isRegistered(name) and EventRegistry.getRegisteredTypes()
are available from Eventador.