{"id":43805660,"url":"https://github.com/openframebox/goevent","last_synced_at":"2026-02-05T22:34:03.105Z","repository":{"id":321361568,"uuid":"1085435769","full_name":"openframebox/goevent","owner":"openframebox","description":"A type-safe, flexible event bus for Go, providing an elegant wrapper around EventBus with enhanced error handling, synchronization, and per-event waiting capabilities.","archived":false,"fork":false,"pushed_at":"2025-12-20T01:39:07.000Z","size":46,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-22T07:14:41.857Z","etag":null,"topics":["event","golang"],"latest_commit_sha":null,"homepage":"https://github.com/openframebox/goevent","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openframebox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-29T03:20:26.000Z","updated_at":"2025-12-20T01:37:43.000Z","dependencies_parsed_at":"2025-10-29T09:07:16.208Z","dependency_job_id":"5240ae81-4f76-4811-8bb4-4302dae2bfbf","html_url":"https://github.com/openframebox/goevent","commit_stats":null,"previous_names":["openframebox/goevent"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/openframebox/goevent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openframebox%2Fgoevent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openframebox%2Fgoevent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openframebox%2Fgoevent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openframebox%2Fgoevent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openframebox","download_url":"https://codeload.github.com/openframebox/goevent/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openframebox%2Fgoevent/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29136789,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T21:59:57.939Z","status":"ssl_error","status_checked_at":"2026-02-05T21:59:57.628Z","response_time":65,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event","golang"],"created_at":"2026-02-05T22:34:03.007Z","updated_at":"2026-02-05T22:34:03.087Z","avatar_url":"https://github.com/openframebox.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GoEvent\n\n[![Go Version](https://img.shields.io/badge/go-%3E%3D1.21-blue.svg)](https://golang.org/doc/devel/release.html)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA **type-safe, flexible event bus** for Go, providing an elegant wrapper around [EventBus](https://github.com/asaskevich/EventBus) with enhanced error handling, synchronization, and per-event waiting capabilities.\n\n## Features\n\n🔒 **Type-Safe**: Interface-based design eliminates reflection in your code\n⚡ **Sync/Async Flexibility**: Choose execution mode per listener\n🎯 **Per-Event Waiting**: Fine-grained control with `DispatchHandle`\n🚨 **Error Collection**: Built-in error tracking and reporting\n🔧 **Simple API**: Minimal boilerplate, maximum flexibility\n✅ **Production Ready**: Thread-safe with proper synchronization\n🌐 **Distributed Support**: Redis driver for multi-process/multi-server event handling\n\n## Installation\n\n```bash\ngo get github.com/openframebox/goevent\n```\n\n## Drivers\n\nGoEvent supports two drivers for different use cases:\n\n### Memory Driver (Default)\nThe default in-memory driver uses EventBus for same-process communication:\n- ✅ Zero configuration required\n- ✅ High performance (no network overhead)\n- ✅ Full DispatchHandle tracking\n- ✅ Support for all Go types\n\n### Redis Driver (Distributed)\nFor distributed event handling across multiple processes or servers:\n- ✅ Multi-process/multi-server support\n- ✅ Redis pub/sub for reliable delivery\n- ✅ JSON serialization for cross-language compatibility\n- ⚠️ Local-only DispatchHandle tracking\n- ⚠️ Requires event type registration\n\n**When to use each:**\n- **Memory Driver**: Single-process applications, high-performance requirements, complex payloads\n- **Redis Driver**: Microservices, distributed systems, worker pools, multi-server deployments\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"github.com/openframebox/goevent\"\n)\n\n// 1. Define your event\ntype UserRegisteredEvent struct {\n    UserID string\n}\n\nfunc (e *UserRegisteredEvent) Name() string {\n    return \"user.registered\"\n}\n\nfunc (e *UserRegisteredEvent) Payload() map[string]any {\n    return map[string]any{\"user_id\": e.UserID}\n}\n\n// 2. Define a listener\ntype EmailNotifier struct{}\n\nfunc (l *EmailNotifier) EventName() string {\n    return \"user.registered\"\n}\n\nfunc (l *EmailNotifier) OnEvent(event goevent.Event) error {\n    e := event.(*UserRegisteredEvent)\n    fmt.Printf(\"Sending email to user: %s\\n\", e.UserID)\n    return nil\n}\n\n// 3. Initialize and use\nfunc main() {\n    evt := goevent.New()\n    evt.RegisterListener(\u0026EmailNotifier{})\n\n    handle := evt.Dispatch(\u0026UserRegisteredEvent{UserID: \"user123\"})\n    handle.Wait() // Wait for completion\n\n    fmt.Println(\"Done!\")\n}\n```\n\n## Redis Driver Usage\n\n### Basic Configuration\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"github.com/openframebox/goevent\"\n)\n\n// 1. Register event types (required for Redis driver)\nfunc init() {\n    goevent.RegisterEventType(\u0026UserRegisteredEvent{})\n}\n\n// 2. Define your event (must be JSON-serializable)\ntype UserRegisteredEvent struct {\n    UserID string `json:\"user_id\"`\n}\n\nfunc (e *UserRegisteredEvent) Name() string {\n    return \"user.registered\"\n}\n\nfunc (e *UserRegisteredEvent) Payload() map[string]any {\n    return map[string]any{\"user_id\": e.UserID}\n}\n\n// 3. Configure Redis driver\nfunc main() {\n    evt := goevent.NewWithConfig(\u0026goevent.Config{\n        Driver: goevent.DriverRedis,\n        Redis: \u0026goevent.RedisConfig{\n            Addr:     \"localhost:6379\",\n            Password: \"\",  // Leave empty if no password\n            DB:       0,\n        },\n    })\n    defer evt.Close() // Always close to cleanup connections\n\n    // Register listeners (same as memory driver)\n    evt.RegisterListener(\u0026EmailNotifier{})\n\n    // Dispatch events (same API)\n    handle := evt.Dispatch(\u0026UserRegisteredEvent{UserID: \"user123\"})\n    handle.Wait()\n\n    fmt.Println(\"Done!\")\n}\n```\n\n### Multi-Process Example\n\n**Process 1 (Publisher):**\n```go\nfunc main() {\n    evt := goevent.NewWithConfig(\u0026goevent.Config{\n        Driver: goevent.DriverRedis,\n        Redis:  \u0026goevent.RedisConfig{Addr: \"localhost:6379\"},\n    })\n    defer evt.Close()\n\n    // Publish events - will be received by all subscribers\n    evt.Dispatch(\u0026OrderCreatedEvent{OrderID: \"123\"})\n}\n```\n\n**Process 2 (Subscriber):**\n```go\nfunc main() {\n    evt := goevent.NewWithConfig(\u0026goevent.Config{\n        Driver: goevent.DriverRedis,\n        Redis:  \u0026goevent.RedisConfig{Addr: \"localhost:6379\"},\n    })\n    defer evt.Close()\n\n    // Register listeners - will receive events from all publishers\n    evt.RegisterListener(\u0026OrderProcessor{})\n    evt.RegisterListener(\u0026EmailSender{})\n\n    // Keep process running\n    select {}\n}\n```\n\n### Cross-Service Communication (Different Codebases)\n\nThe Redis driver supports **true cross-service communication** where different services can have their own event definitions:\n\n**Service A (Order Service):**\n```go\npackage main\n\nimport \"github.com/openframebox/goevent\"\n\ntype OrderCreatedEvent struct {\n    OrderID string `json:\"order_id\"`\n}\n\nfunc (e *OrderCreatedEvent) Name() string {\n    return \"order.created\"  // Key for cross-service compatibility\n}\n\nfunc init() {\n    goevent.RegisterEventType(\u0026OrderCreatedEvent{})\n}\n\nfunc main() {\n    evt := goevent.NewWithConfig(\u0026goevent.Config{\n        Driver: goevent.DriverRedis,\n        Redis:  \u0026goevent.RedisConfig{Addr: \"redis:6379\"},\n    })\n\n    evt.Dispatch(\u0026OrderCreatedEvent{OrderID: \"123\"})\n}\n```\n\n**Service B (Email Service - completely different codebase):**\n```go\npackage main\n\nimport \"github.com/openframebox/goevent\"\n\n// Same logical event, different package, SAME Name()\ntype OrderCreatedEvent struct {\n    OrderID string `json:\"order_id\"`\n}\n\nfunc (e *OrderCreatedEvent) Name() string {\n    return \"order.created\"  // SAME name = cross-service compatible!\n}\n\nfunc init() {\n    goevent.RegisterEventType(\u0026OrderCreatedEvent{})\n}\n\ntype EmailListener struct{}\n\nfunc (l *EmailListener) EventName() string {\n    return \"order.created\"\n}\n\nfunc (l *EmailListener) OnEvent(event goevent.Event) error {\n    e := event.(*OrderCreatedEvent)  // ✅ Type-safe!\n    sendEmail(e.OrderID)\n    return nil\n}\n```\n\n**How it works:** Both services register with `event.Name()` = `\"order.created\"`, so deserialization works across services even though they're different packages/codebases!\n\n### Redis Configuration Options\n\n```go\nevt := goevent.NewWithConfig(\u0026goevent.Config{\n    Driver: goevent.DriverRedis,\n    Redis: \u0026goevent.RedisConfig{\n        // Connection\n        Addr:     \"localhost:6379\",\n        Password: \"your-password\",\n        DB:       0,\n\n        // Pub/Sub\n        ChannelPrefix: \"myapp:\",  // Prefix for Redis channels (default: \"goevent:\")\n\n        // Performance\n        MaxEventSize: 1024 * 1024,  // Max event size in bytes (default: 1MB)\n        PoolSize:     10,           // Connection pool size (default: 10)\n        MinIdleConns: 5,            // Min idle connections (default: 0)\n\n        // Timeouts\n        DialTimeout:  5 * time.Second,\n        ReadTimeout:  3 * time.Second,\n        WriteTimeout: 3 * time.Second,\n\n        // TLS (optional)\n        TLSConfig: \u0026tls.Config{...},\n    },\n})\n```\n\n### Important: Event Registration\n\n**All event types MUST be registered** when using the Redis driver:\n\n```go\nfunc init() {\n    // Register all event types used in your application\n    goevent.RegisterEventType(\u0026UserCreatedEvent{})\n    goevent.RegisterEventType(\u0026OrderProcessedEvent{})\n    goevent.RegisterEventType(\u0026PaymentReceivedEvent{})\n}\n```\n\n**How it works:** `RegisterEventType()` uses `event.Name()` as the registry key. This enables **cross-service communication** - different services can have the same logical event with different package names, as long as they return the same value from `Name()`.\n\n**For custom type names or versioning:**\n```go\nfunc init() {\n    // Use custom type name (e.g., for versioning)\n    goevent.RegisterEventTypeAs(\"order.created.v1\", \u0026OrderCreatedEventV1{})\n    goevent.RegisterEventTypeAs(\"order.created.v2\", \u0026OrderCreatedEventV2{})\n}\n```\n\nWithout registration, deserialization will fail with a clear error message.\n\n### Redis Driver Limitations\n\n1. **Local-Only DispatchHandle Tracking**\n   - `handle.Wait()` only waits for LOCAL handlers in the current process\n   - Remote handlers in other processes are not tracked\n   - Use for synchronization within a process, not across processes\n\n2. **Local-Only Error Collection**\n   - `handle.GetErrors()` only returns errors from LOCAL handlers\n   - Errors from remote processes are not collected\n   - Use centralized logging for distributed error tracking\n\n3. **JSON Serialization Requirements**\n   - Event payloads must be JSON-serializable\n   - Complex types (functions, channels, unexported fields) are not supported\n   - Use struct tags for custom field names: `json:\"field_name\"`\n\n4. **Network Latency**\n   - Redis driver adds network overhead vs in-memory\n   - Typical latency: 1-5ms depending on network\n   - Use for distributed systems where latency is acceptable\n\n### Switching Between Drivers\n\nThe API is identical for both drivers - just change the configuration:\n\n```go\n// Development: use memory driver\nevt := goevent.New()\n\n// Production: use Redis driver\nevt := goevent.NewWithConfig(\u0026goevent.Config{\n    Driver: goevent.DriverRedis,\n    Redis:  \u0026goevent.RedisConfig{Addr: os.Getenv(\"REDIS_ADDR\")},\n})\n```\n\n## Usage Examples\n\n### Synchronous vs Asynchronous Listeners\n\nBy default, listeners execute **synchronously**. To make a listener async, implement the `ListenerWithOptions` interface:\n\n```go\n// Synchronous listener (default)\ntype SyncListener struct{}\n\nfunc (l *SyncListener) EventName() string {\n    return \"my.event\"\n}\n\nfunc (l *SyncListener) OnEvent(event goevent.Event) error {\n    // Executes immediately in the same goroutine\n    return nil\n}\n\n// Asynchronous listener\ntype AsyncListener struct{}\n\nfunc (l *AsyncListener) EventName() string {\n    return \"my.event\"\n}\n\nfunc (l *AsyncListener) OnEvent(event goevent.Event) error {\n    // Executes in a separate goroutine\n    return nil\n}\n\n// This makes it async!\nfunc (l *AsyncListener) Options() goevent.ListenerOptions {\n    return goevent.ListenerOptions{Async: true}\n}\n```\n\n### Per-Event Waiting with DispatchHandle\n\nEach `Dispatch()` returns a handle for fine-grained control:\n\n```go\n// Wait for a specific event\nhandle := evt.Dispatch(\u0026CriticalEvent{})\nhandle.Wait() // Blocks until this event's handlers complete\n\n// Check errors for this specific dispatch\nif errs := handle.GetErrors(); len(errs) \u003e 0 {\n    log.Printf(\"Errors occurred: %v\", errs)\n}\n\n// Non-blocking check with Done() channel\nhandle := evt.Dispatch(\u0026Event{})\nselect {\ncase \u003c-handle.Done():\n    fmt.Println(\"Completed!\")\ncase \u003c-time.After(timeout):\n    fmt.Println(\"Timeout!\")\n}\n```\n\n### Fire-and-Forget Pattern\n\nFor non-critical events, simply discard the handle:\n\n```go\n// Dispatch and continue immediately\nevt.Dispatch(\u0026AnalyticsEvent{})\nevt.Dispatch(\u0026LogEvent{})\n// Handlers run in background, no waiting\n\n// At shutdown, wait for all remaining handlers\ndefer evt.Wait()\n```\n\n### Error Handling\n\n```go\n// Per-dispatch errors\nhandle := evt.Dispatch(\u0026Event{})\nhandle.Wait()\nfor _, err := range handle.GetErrors() {\n    log.Printf(\"Handler error: %s\", err)\n}\n\n// Global error collection\nevt.Dispatch(\u0026Event1{})\nevt.Dispatch(\u0026Event2{})\nevt.Wait()\n\n// Get all errors across all dispatches\nallErrors := evt.GetErrors()\nfmt.Printf(\"Total errors: %d\\n\", len(allErrors))\n\n// Clear errors\nevt.ClearErrors()\n```\n\n### Hybrid Pattern (Recommended)\n\nCombine per-event and global waiting for maximum flexibility:\n\n```go\nfunc ProcessOrder(orderID string) error {\n    // Critical: Must complete before continuing\n    handle := evt.Dispatch(\u0026ProcessPaymentEvent{OrderID: orderID})\n    handle.Wait()\n\n    if errs := handle.GetErrors(); len(errs) \u003e 0 {\n        return fmt.Errorf(\"payment failed: %v\", errs[0])\n    }\n\n    // Non-critical: Fire and forget\n    evt.Dispatch(\u0026SendReceiptEmail{OrderID: orderID})\n    evt.Dispatch(\u0026UpdateAnalytics{OrderID: orderID})\n\n    return nil\n}\n\nfunc main() {\n    defer evt.Wait() // Catch any remaining async handlers at shutdown\n\n    // Your application logic...\n}\n```\n\n### Using Event Payloads\n\nAccess event data through the `Payload()` method:\n\n```go\ntype OrderCreatedEvent struct {\n    OrderID string\n    Amount  float64\n}\n\nfunc (e *OrderCreatedEvent) Name() string {\n    return \"order.created\"\n}\n\nfunc (e *OrderCreatedEvent) Payload() map[string]any {\n    return map[string]any{\n        \"order_id\": e.OrderID,\n        \"amount\":   e.Amount,\n    }\n}\n\n// In your listener\nfunc (l *Listener) OnEvent(event goevent.Event) error {\n    payload := event.Payload()\n    orderID := payload[\"order_id\"].(string)\n    amount := payload[\"amount\"].(float64)\n\n    // Or use type assertion\n    if e, ok := event.(*OrderCreatedEvent); ok {\n        fmt.Printf(\"Order %s: $%.2f\\n\", e.OrderID, e.Amount)\n    }\n\n    return nil\n}\n```\n\n### Unsubscribing Listeners\n\nYou can remove all listeners for a specific event at runtime using `UnregisterListenersForEvent()`:\n\n```go\n// Register listeners\nevt.RegisterListener(\u0026EmailListener{})\nevt.RegisterListener(\u0026SMSListener{})\n\n// Dispatch events - both listeners handle them\nevt.Dispatch(\u0026UserCreatedEvent{})\n\n// Later: disable all listeners for this event\nevt.UnregisterListenersForEvent(\"user.created\")\n\n// Dispatch again - no listeners will handle it\nevt.Dispatch(\u0026UserCreatedEvent{})  // Nothing happens\n```\n\n**Common use cases:**\n- **Feature Flags**: Dynamically enable/disable event-driven features\n- **Maintenance Mode**: Temporarily disable certain handlers during maintenance\n- **Testing**: Clean up listeners between test cases\n- **Dynamic Configuration**: Enable/disable integrations at runtime\n\n```go\n// Example: Feature flag integration\nfunc UpdateFeatureFlags(flags map[string]bool) {\n    if !flags[\"email_notifications\"] {\n        evt.UnregisterListenersForEvent(\"user.created\")\n        evt.UnregisterListenersForEvent(\"order.created\")\n    }\n\n    if !flags[\"analytics\"] {\n        evt.UnregisterListenersForEvent(\"page.viewed\")\n        evt.UnregisterListenersForEvent(\"button.clicked\")\n    }\n}\n```\n\n**Important notes:**\n- ✅ **Thread-safe**: Can be called while events are being dispatched\n- ✅ **Idempotent**: Safe to call multiple times for the same event\n- ✅ **Removes ALL listeners**: All listeners for the specified event are removed\n- ✅ **Re-registerable**: You can register listeners again after unsubscribing\n\n```go\n// Idempotent - safe to call multiple times\nevt.UnregisterListenersForEvent(\"user.created\")\nevt.UnregisterListenersForEvent(\"user.created\")  // No error\n\n// Non-existent events - no error\nevt.UnregisterListenersForEvent(\"never.registered\")  // No error\n\n// Re-register after unsubscribe\nevt.UnregisterListenersForEvent(\"user.created\")\nevt.RegisterListener(\u0026NewEmailListener{})  // Works fine\nevt.Dispatch(\u0026UserCreatedEvent{})  // NewEmailListener handles it\n```\n\n**Works with both drivers:**\n```go\n// Memory driver\nevt := goevent.New()\nevt.RegisterListener(\u0026Listener{})\nevt.UnregisterListenersForEvent(\"my.event\")\n\n// Redis driver\nevt := goevent.NewWithConfig(\u0026goevent.Config{\n    Driver: goevent.DriverRedis,\n    Redis:  \u0026goevent.RedisConfig{Addr: \"localhost:6379\"},\n})\nevt.RegisterListener(\u0026Listener{})\nevt.UnregisterListenersForEvent(\"my.event\")  // Closes Redis subscription\n```\n\n## API Reference\n\n### Core Types\n\n```go\ntype Event interface {\n    Name() string\n    Payload() map[string]any\n}\n\ntype Listener interface {\n    EventName() string\n    OnEvent(event Event) error\n}\n\ntype ListenerWithOptions interface {\n    Listener\n    Options() ListenerOptions\n}\n\ntype ListenerOptions struct {\n    Async bool  // Execute asynchronously if true\n}\n```\n\n### GoEvent Methods\n\n```go\n// Constructors\nfunc New() *GoEvent  // Creates with memory driver (backward compatible)\nfunc NewWithConfig(cfg *Config) *GoEvent  // Creates with custom driver\n\n// Core methods\nfunc (ge *GoEvent) RegisterListener(listeners ...Listener)\nfunc (ge *GoEvent) UnregisterListenersForEvent(eventName string) error  // Remove all listeners for an event\nfunc (ge *GoEvent) Dispatch(event Event) *DispatchHandle\nfunc (ge *GoEvent) Wait()\nfunc (ge *GoEvent) GetErrors() []*EventError\nfunc (ge *GoEvent) ClearErrors()\nfunc (ge *GoEvent) Close() error  // Cleanup resources (important for Redis driver)\n```\n\n### Configuration Types\n\n```go\ntype Config struct {\n    Driver DriverType    // DriverMemory or DriverRedis\n    Redis  *RedisConfig  // Required when Driver is DriverRedis\n}\n\ntype DriverType string\nconst (\n    DriverMemory DriverType = \"memory\"\n    DriverRedis  DriverType = \"redis\"\n)\n\ntype RedisConfig struct {\n    // Connection\n    Addr     string\n    Password string\n    DB       int\n\n    // Pub/Sub\n    ChannelPrefix string  // Default: \"goevent:\"\n\n    // Performance\n    MaxEventSize int      // Default: 1MB\n    PoolSize     int      // Default: 10\n    MinIdleConns int      // Default: 0\n\n    // Timeouts\n    DialTimeout  time.Duration  // Default: 5s\n    ReadTimeout  time.Duration  // Default: 3s\n    WriteTimeout time.Duration  // Default: 3s\n\n    // TLS (optional)\n    TLSConfig *tls.Config\n}\n```\n\n### Event Registration (Redis Driver)\n\n```go\n// Register using event.Name() as key (recommended for cross-service)\nfunc RegisterEventType(event Event)\n\n// Register with custom type name (for versioning, custom names)\nfunc RegisterEventTypeAs(typeName string, event Event)\n```\n\n### DispatchHandle Methods\n\n```go\nfunc (dh *DispatchHandle) Wait()\nfunc (dh *DispatchHandle) Done() \u003c-chan struct{}\nfunc (dh *DispatchHandle) GetErrors() []*EventError\n```\n\n## Real-World Example\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"syscall\"\n\n    \"github.com/openframebox/goevent\"\n)\n\n// Global event bus\nvar Evt = goevent.New()\n\nfunc init() {\n    // Register all listeners at startup\n    Evt.RegisterListener(\n        \u0026PaymentProcessor{},\n        \u0026EmailSender{},\n        \u0026AnalyticsTracker{},\n    )\n}\n\nfunc main() {\n    // Graceful shutdown\n    sigChan := make(chan os.Signal, 1)\n    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n\n    go func() {\n        \u003c-sigChan\n        log.Println(\"Shutting down gracefully...\")\n        Evt.Wait() // Wait for all pending event handlers\n        os.Exit(0)\n    }()\n\n    // Your application logic...\n    ProcessOrder(\"order-123\")\n\n    // Keep running\n    select {}\n}\n\nfunc ProcessOrder(orderID string) error {\n    // Critical event - must wait\n    handle := Evt.Dispatch(\u0026PaymentEvent{OrderID: orderID})\n    handle.Wait()\n\n    if errs := handle.GetErrors(); len(errs) \u003e 0 {\n        return errs[0].Err\n    }\n\n    // Non-critical events - fire and forget\n    Evt.Dispatch(\u0026EmailEvent{OrderID: orderID})\n    Evt.Dispatch(\u0026AnalyticsEvent{OrderID: orderID})\n\n    return nil\n}\n```\n\n## Why GoEvent?\n\n### vs. EventBus (underlying library)\n- ✅ Type-safe interfaces instead of reflection\n- ✅ Built-in error collection and reporting\n- ✅ Per-event waiting and tracking\n- ✅ Simplified async/sync configuration\n\n### vs. Channels\n- ✅ Multiple listeners per event automatically\n- ✅ No manual goroutine management\n- ✅ Built-in error handling\n- ✅ More declarative code\n\n## Contributing\n\nContributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## Acknowledgments\n\nBuilt on top of [asaskevich/EventBus](https://github.com/asaskevich/EventBus)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenframebox%2Fgoevent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenframebox%2Fgoevent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenframebox%2Fgoevent/lists"}