12 KiB
12 KiB
Events Package Specification
Overview
The Events package provides a robust event system that matches Laravel's event functionality while leveraging Dart's async capabilities. It integrates with our Queue, Bus, and Database packages to provide a complete event handling solution.
Related Documentation
- See Laravel Compatibility Roadmap for implementation status
- See Foundation Integration Guide for integration patterns
- See Testing Guide for testing approaches
- See Getting Started Guide for development setup
- See Contracts Package Specification for event contracts
Core Features
1. Event Dispatcher
/// Core event dispatcher implementation
class EventDispatcher implements EventDispatcherContract {
final Container _container;
final Map<Type, List<EventListener>> _listeners = {};
final List<EventSubscriber> _subscribers = {};
final QueueContract? _queue;
final BroadcasterContract? _broadcaster;
final List<dynamic> _afterCommitEvents = [];
EventDispatcher(
this._container, {
QueueContract? queue,
BroadcasterContract? broadcaster
}) : _queue = queue,
_broadcaster = broadcaster;
@override
void listen<T>(void Function(T event) listener) {
_listeners.putIfAbsent(T, () => []).add(
EventListener<T>(listener)
);
}
@override
Future<void> dispatch<T>(T event) async {
var listeners = _listeners[T] ?? [];
// Handle after commit events
if (event is ShouldDispatchAfterCommit && _isWithinTransaction()) {
_afterCommitEvents.add(event);
return;
}
// Handle queued events
if (event is ShouldQueue && _queue != null) {
await _queueEvent(event, listeners);
return;
}
// Handle broadcasting
if (event is ShouldBroadcast && _broadcaster != null) {
await _broadcastEvent(event);
}
// Notify listeners
await _notifyListeners(event, listeners);
}
@override
Future<void> dispatchAfterCommit<T>(T event) async {
if (_isWithinTransaction()) {
_afterCommitEvents.add(event);
} else {
await dispatch(event);
}
}
bool _isWithinTransaction() {
if (_container.has<DatabaseManager>()) {
var db = _container.make<DatabaseManager>();
return db.transactionLevel > 0;
}
return false;
}
Future<void> _dispatchAfterCommitEvents() async {
var events = List.from(_afterCommitEvents);
_afterCommitEvents.clear();
for (var event in events) {
await dispatch(event);
}
}
}
2. Event Discovery
/// Discovers event handlers through reflection and attributes
class EventDiscovery {
final Container _container;
final Reflector _reflector;
EventDiscovery(this._container, this._reflector);
/// Discovers event handlers in a directory
Future<void> discoverEvents(String path) async {
var files = Directory(path).listSync(recursive: true);
for (var file in files) {
if (file.path.endsWith('.dart')) {
await _processFile(file.path);
}
}
}
Future<void> _processFile(String path) async {
var library = await _reflector.loadLibrary(path);
for (var type in library.declarations.values) {
if (type is ClassMirror) {
_processClass(type);
}
}
}
void _processClass(ClassMirror classMirror) {
// Find @Handles annotations
for (var method in classMirror.declarations.values) {
if (method is MethodMirror) {
var handles = method.metadata
.firstWhere((m) => m.type == Handles,
orElse: () => null);
if (handles != null) {
var eventType = handles.getField('event').reflectee;
_registerHandler(classMirror.reflectedType, method.simpleName, eventType);
}
}
}
}
void _registerHandler(Type classType, Symbol methodName, Type eventType) {
var instance = _container.make(classType);
var dispatcher = _container.make<EventDispatcherContract>();
dispatcher.listen(eventType, (event) {
var mirror = reflect(instance);
mirror.invoke(methodName, [event]);
});
}
}
3. Event Broadcasting
/// Contract for event broadcasters
abstract class BroadcasterContract {
/// Broadcasts an event
Future<void> broadcast(
List<String> channels,
String eventName,
dynamic data
);
/// Creates a private channel
Channel privateChannel(String name);
/// Creates a presence channel
PresenceChannel presenceChannel(String name);
}
/// Pusher event broadcaster
class PusherBroadcaster implements BroadcasterContract {
final PusherClient _client;
final AuthManager _auth;
PusherBroadcaster(this._client, this._auth);
@override
Future<void> broadcast(
List<String> channels,
String eventName,
dynamic data
) async {
for (var channel in channels) {
await _client.trigger(channel, eventName, data);
}
}
@override
Channel privateChannel(String name) {
return PrivateChannel(_client, _auth, name);
}
@override
PresenceChannel presenceChannel(String name) {
return PresenceChannel(_client, _auth, name);
}
}
4. Integration with Queue
/// Job for processing queued events
class QueuedEventJob implements Job {
final dynamic event;
final List<EventListener> listeners;
final Map<String, dynamic> data;
QueuedEventJob({
required this.event,
required this.listeners,
this.data = const {}
});
@override
Future<void> handle() async {
for (var listener in listeners) {
try {
await listener.handle(event);
} catch (e) {
await _handleFailure(e);
}
}
}
@override
Future<void> failed([Exception? e]) async {
if (event is FailedEventHandler) {
await (event as FailedEventHandler).failed(e);
}
}
@override
int get tries => event is HasTries ? (event as HasTries).tries : 1;
@override
Duration? get timeout =>
event is HasTimeout ? (event as HasTimeout).timeout : null;
}
5. Integration with Bus
/// Event command for command bus integration
class EventCommand implements Command {
final dynamic event;
final List<EventListener> listeners;
EventCommand(this.event, this.listeners);
@override
Type get handler => EventCommandHandler;
}
/// Handler for event commands
class EventCommandHandler implements Handler<EventCommand> {
final EventDispatcher _events;
EventCommandHandler(this._events);
@override
Future<void> handle(EventCommand command) async {
await _events._notifyListeners(
command.event,
command.listeners
);
}
}
Usage Examples
Basic Event Handling
// Define event
class OrderShipped {
final Order order;
OrderShipped(this.order);
}
// Create listener
@Handles(OrderShipped)
class OrderShippedListener {
void handle(OrderShipped event) {
// Handle event
}
}
// Register and dispatch
dispatcher.listen<OrderShipped>((event) {
// Handle event
});
await dispatcher.dispatch(OrderShipped(order));
After Commit Events
class OrderCreated implements ShouldDispatchAfterCommit {
final Order order;
OrderCreated(this.order);
}
// In transaction
await db.transaction((tx) async {
var order = await tx.create(orderData);
await dispatcher.dispatchAfterCommit(OrderCreated(order));
});
Broadcasting
class MessageSent implements ShouldBroadcast {
final Message message;
@override
List<String> broadcastOn() => [
'private-chat.${message.roomId}'
];
@override
Map<String, dynamic> get broadcastWith => {
'id': message.id,
'content': message.content,
'user': message.user.toJson()
};
}
// Create private channel
var channel = broadcaster.privateChannel('chat.123');
await channel.whisper('typing', {'user': 'john'});
Queue Integration
class ProcessOrder implements ShouldQueue {
final Order order;
@override
String get queue => 'orders';
@override
int get tries => 3;
@override
Duration get timeout => Duration(minutes: 5);
}
// Dispatch queued event
await dispatcher.dispatch(ProcessOrder(order));
Testing
void main() {
group('Event Dispatcher', () {
test('dispatches after commit', () async {
var dispatcher = MockEventDispatcher();
var db = MockDatabase();
await db.transaction((tx) async {
await dispatcher.dispatchAfterCommit(OrderShipped(order));
expect(dispatcher.hasAfterCommitEvents, isTrue);
});
expect(dispatcher.hasAfterCommitEvents, isFalse);
verify(() => dispatcher.dispatch(any())).called(1);
});
test('discovers event handlers', () async {
var discovery = EventDiscovery(container, reflector);
await discovery.discoverEvents('lib/events');
var dispatcher = container.make<EventDispatcherContract>();
await dispatcher.dispatch(OrderShipped(order));
verify(() => orderListener.handle(any())).called(1);
});
});
}
Next Steps
- Complete after commit handling
- Enhance event discovery
- Add more broadcast drivers
- Improve queue integration
- Add performance optimizations
- Write more tests
Would you like me to enhance any other package specifications?
Development Guidelines
1. Getting Started
Before implementing event features:
- Review Getting Started Guide
- Check Laravel Compatibility Roadmap
- Follow Testing Guide
- Use Foundation Integration Guide
- Understand Contracts Package Specification
2. Implementation Process
For each event feature:
- Write tests following Testing Guide
- Implement following Laravel patterns
- Document following Getting Started Guide
- Integrate following Foundation Integration Guide
3. Quality Requirements
All implementations must:
- Pass all tests (see Testing Guide)
- Meet Laravel compatibility requirements
- Follow integration patterns (see Foundation Integration Guide)
- Implement required contracts (see Contracts Package Specification)
4. Integration Considerations
When implementing events:
- Follow patterns in Foundation Integration Guide
- Ensure Laravel compatibility per Laravel Compatibility Roadmap
- Use testing approaches from Testing Guide
- Follow development setup in Getting Started Guide
- Implement all contracts from Contracts Package Specification
5. Performance Guidelines
Event system must:
- Handle high throughput efficiently
- Minimize memory usage
- Support async operations
- Scale horizontally
- Meet performance targets in Laravel Compatibility Roadmap
6. Testing Requirements
Event tests must:
- Cover all event scenarios
- Test async behavior
- Verify queue integration
- Check broadcasting
- Follow patterns in Testing Guide
7. Documentation Requirements
Event documentation must:
- Explain event patterns
- Show integration examples
- Cover error handling
- Include performance tips
- Follow standards in Getting Started Guide