Skip to content

Actor system integration

This page shows the two ways to host libspiffy’s actors, standalone or inside your own Dactor actor system, and how your code reaches them through the coordinator in either mode.

If you pass no actorSystem to initialize(), libspiffy creates its own LocalActorSystem and owns it. shutdown() shuts that system down too. This is the mode to use unless your app already runs Dactor actors.

final libspiffy = LibSpiffyActorSystem();
await libspiffy.initialize(networkType: 'test', isar: isar);
// ... use libspiffy.coordinator and libspiffy.coordinatorEvents ...
await libspiffy.shutdown(); // stops libspiffy's actors and its actor system

initialize() also takes config:, an ActorSystemConfig for the system libspiffy creates.

libspiffy also keeps one process-wide instance behind three functions, used by the README and older code:

await initializeLibSpiffy(networkType: 'test', isar: isar);
final balance =
await getLibSpiffySystem().coordinator.ask(GetBalanceQuery(walletId: 'shop'));
await shutdownLibSpiffy();

initializeLibSpiffy() forwards only some of initialize()’s parameters: actorSystem, dataDirectory, config, readModelStorage, secureStorage, cryptoService, arcConfig, isar, networkType, enableP2P, startHeight, peerAddresses, userAgent, channelPeerId and channelTiming. For PostgreSQL, a header CDN or a blockchain data source, create a LibSpiffyActorSystem and call initialize() on it.

If your app already has a Dactor actor system, pass it as actorSystem. libspiffy spawns its actors there instead of creating a system of its own: one supervision tree, one dispatcher, and your actors can hold libspiffy’s coordinator directly.

lib/main.dart
import 'dart:io';
import 'package:dactor/dactor.dart';
import 'package:isar_community/isar.dart';
import 'package:libspiffy/coordinator.dart';
import 'package:libspiffy/libspiffy.dart';
/// Your own actor. It asks libspiffy's coordinator for an invoice.
class CheckoutActor extends Actor {
CheckoutActor(this._coordinator);
final WalletCoordinator _coordinator;
@override
Future<void> onMessage(dynamic message) async {
if (message is StartCheckout) {
try {
final invoice = await _coordinator.ask(CreateInvoiceCommand(
walletId: message.walletId,
amount: message.amount,
description: message.orderId,
));
print('order ${message.orderId}: invoice ${invoice.invoiceId}');
} on CoordinatorFailure catch (failure) {
print('order ${message.orderId}: no invoice (${failure.message})');
}
}
}
}
class StartCheckout implements Message {
StartCheckout(this.walletId, this.orderId, this.amount);
final String walletId;
final String orderId;
final BigInt amount;
@override
String get correlationId => 'checkout-$orderId';
@override
Map<String, dynamic> get metadata => {'orderId': orderId};
@override
ActorRef? get replyTo => null;
@override
DateTime get timestamp => DateTime.now();
}
Future<void> main() async {
final hostSystem = LocalActorSystem(ActorSystemConfig());
// One Isar instance for your data and libspiffy's.
await Directory('./app-data').create(recursive: true);
await Isar.initializeIsarCore(download: true);
final isar = await Isar.open(
[...LibSpiffySchemas.allSchemas /*, ...your own schemas */],
directory: './app-data',
);
final libspiffy = LibSpiffyActorSystem();
await libspiffy.initialize(
actorSystem: hostSystem,
isar: isar,
networkType: 'test',
);
final checkout = await hostSystem.spawn(
'checkout',
() => CheckoutActor(libspiffy.coordinator),
);
checkout.tell(StartCheckout('shop', 'order-1001', BigInt.from(5000)));
// ...
// Shut down in reverse order. libspiffy leaves the host's resources alone.
await libspiffy.shutdown(); // stops libspiffy's actors only
await isar.close(); // the host opened it, the host closes it
await hostSystem.shutdown();
}

Things to know in this mode:

  • shutdown() stops libspiffy’s actors and projections but does not shut down your actor system, and does not close an Isar instance you passed. Shut libspiffy down first, then close Isar, then your system.
  • Hand your actors the WalletCoordinator (libspiffy.coordinator), not an ActorRef. The coordinator does not reply to the sender of a message: an actor that needs the reply awaits ask(), as CheckoutActor does, and follows unrequested events with on<E>().
  • Your actor names must not clash with libspiffy’s: wallet-coordinator, wallet-manager, invoice-coordinator, spv-actor, header-sync, arc-actor, payment-coordinator, benford-coordinator, payment-channel-manager, import-actor, and the three projection-... actors. The architecture overview lists them.
  • Open your Isar instance with LibSpiffySchemas.allSchemas, not only LibSpiffySchemas.walletSchemas. initialize() throws an ArgumentError naming the missing collections otherwise: libspiffy needs the event store, projection checkpoint and broadcast queue collections too.
  • In a pure Dart program, call Isar.initializeIsarCore(download: true) before Isar.open, and make sure the directory exists. Flutter apps get the Isar core from isar_community_flutter_libs.

In both modes your code talks to one actor, WalletCoordinatorActor, through libspiffy.coordinator, a WalletCoordinator. You do not need a gateway actor of your own. ask() gives you the reply to each request you send; on<E>() gives you what happens without a request.

// Sketch: requests through ask(), everything else through on<E>().
final coordinator = libspiffy.coordinator;
coordinator.on<InvoicePaidEvent>().listen(onInvoicePaid);
coordinator.on<ChannelRequestReceivedEvent>().listen(onChannelRequest);
coordinator.on<ErrorEvent>().listen(onError);
final payment = await coordinator.ask(PayInvoiceCommand(
walletId: 'shop',
invoiceId: invoiceId,
addresses: addresses,
amount: amount,
));
sendToPayee(payment.beefBytes);

libspiffy.coordinatorEvents is the whole stream, replies included, if you prefer one listener that switches on the event type.

LibSpiffyActorSystem also exposes the internal actors (walletManager, invoiceCoordinator, paymentCoordinator, spvActor, arcActor, headerSyncActor, channelManager). Messages you send them bypass the coordinator’s correlation tracking, the coordinator publishes no reply for them, and ask() cannot wait on them. Use them only for diagnostics or advanced extensions.

if (libspiffy.ownsActorSystem) {
print('standalone: libspiffy created its actor system');
} else {
print('integrated: running in the host actor system');
}
final system = libspiffy.actorSystem; // the system libspiffy's actors live in

ownsActorSystem is false when you passed actorSystem. The actorSystem getter throws a StateError before initialize() and after shutdown().

LibSpiffyActorSystem runs in whichever isolate calls initialize(), and its actors, streams and Isar instance cannot cross an isolate boundary. To keep it off a Flutter UI thread, see Run in a background isolate.