Implement HookEvent data model and LifecycleHooks registry with on/once/off/emit API #83

Closed
opened 2026-03-15 04:13:03 +00:00 by freemo · 1 comment
Owner

Metadata

  • Commit Message: feat(lifecycle): add HookEvent and LifecycleHooks registry
  • Branch: feature/m1-lifecycle-hooks

Background and Context

The specification describes a HookEvent data model carrying type, source, data,
and cancelled fields, and a LifecycleHooks singleton registry with on, once,
off, and emit methods. This is the foundation for all lifecycle hook functionality.

Expected Behavior

  • HookEvent is an immutable data object with type (Symbol), source (Object),
    data (Hash), and cancelled (Boolean) fields
  • LifecycleHooks.on(hook_name, &block) registers a persistent callback
  • LifecycleHooks.once(hook_name, &block) registers a one-shot callback
  • LifecycleHooks.off(hook_name, &block) removes a callback
  • LifecycleHooks.emit(hook_name, source:, data: {}) fires all registered callbacks

Acceptance Criteria

  • HookEvent class in lib/aethyr/core/components/lifecycle/hook_event.rb
  • LifecycleHooks module in lib/aethyr/core/components/lifecycle/lifecycle_hooks.rb
  • on/once/off/emit methods work correctly
  • Thread-safe callback storage (Mutex-protected)
  • BDD scenarios covering registration, emission, once-only, and removal

Subtasks

  • Create HookEvent data class
  • Create LifecycleHooks registry module
  • Implement on, once, off, emit methods
  • Add thread-safety with Mutex
  • Write Cucumber feature file and step definitions

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the
    Commit Message in Metadata exactly.
  • The commit is pushed to the remote on the branch matching the Branch in Metadata.
  • The commit is submitted as a pull request to master, reviewed, and merged.
## Metadata - **Commit Message**: `feat(lifecycle): add HookEvent and LifecycleHooks registry` - **Branch**: `feature/m1-lifecycle-hooks` ## Background and Context The specification describes a `HookEvent` data model carrying `type`, `source`, `data`, and `cancelled` fields, and a `LifecycleHooks` singleton registry with `on`, `once`, `off`, and `emit` methods. This is the foundation for all lifecycle hook functionality. ## Expected Behavior - `HookEvent` is an immutable data object with `type` (Symbol), `source` (Object), `data` (Hash), and `cancelled` (Boolean) fields - `LifecycleHooks.on(hook_name, &block)` registers a persistent callback - `LifecycleHooks.once(hook_name, &block)` registers a one-shot callback - `LifecycleHooks.off(hook_name, &block)` removes a callback - `LifecycleHooks.emit(hook_name, source:, data: {})` fires all registered callbacks ## Acceptance Criteria - `HookEvent` class in `lib/aethyr/core/components/lifecycle/hook_event.rb` - `LifecycleHooks` module in `lib/aethyr/core/components/lifecycle/lifecycle_hooks.rb` - `on`/`once`/`off`/`emit` methods work correctly - Thread-safe callback storage (Mutex-protected) - BDD scenarios covering registration, emission, once-only, and removal ## Subtasks - [x] Create `HookEvent` data class - [x] Create `LifecycleHooks` registry module - [x] Implement `on`, `once`, `off`, `emit` methods - [x] Add thread-safety with Mutex - [x] Write Cucumber feature file and step definitions ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly. - The commit is pushed to the remote on the branch matching the **Branch** in Metadata. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged**.
freemo added this to the v1.3.0 milestone 2026-03-15 04:13:03 +00:00
freemo self-assigned this 2026-03-15 04:25:23 +00:00
freemo modified the milestone from v1.3.0 to v1.0.0 2026-03-16 00:27:56 +00:00
Author
Owner

Implementation Notes

Design Decisions

  1. HookEvent as frozen value object: The HookEvent class freezes both itself and its data hash on initialization, ensuring immutability. This prevents handlers from accidentally mutating shared event state.

  2. LifecycleHooks as module with class-level methods: Using class << self on a module gives us singleton behavior without requiring explicit instantiation. The reset! method supports test isolation.

  3. Mutex-based thread safety: All callback storage mutations are protected by a Mutex. The emit method takes a snapshot (.dup) of the callback list before iteration to avoid holding the lock during callback execution, which prevents deadlocks if a callback registers/removes other callbacks.

  4. Block-identity-based off: The off method matches callbacks by block object identity (==), consistent with the issue requirements. Callers must retain a reference to the original block to remove it.

  5. Extracted private methods for Rubocop compliance: The emit method was refactored to delegate argument validation (validate_emit_args!), callback dispatch (dispatch_callbacks), and one-shot cleanup (remove_once_entry) to private methods, satisfying Metrics/AbcSize and Metrics/MethodLength constraints.

Files Created

File Purpose
lib/aethyr/core/components/lifecycle/hook_event.rb Immutable HookEvent data class
lib/aethyr/core/components/lifecycle/lifecycle_hooks.rb LifecycleHooks singleton registry
tests/unit/lifecycle_hooks.feature 20 Cucumber scenarios
tests/unit/step_definitions/lifecycle_hooks_steps.rb Step definitions with custom :symbol ParameterType

Test Results

  • 20 scenarios, 64 steps — all passing
  • Rubocop: 0 offenses on new files
  • Integration tests: Pre-existing failure unrelated to this change (1 failed scenario existed before this branch)

Custom ParameterType

Added a symbol ParameterType to Cucumber for matching Ruby symbol literals (:on_boot, :on_tick, etc.) in Gherkin steps. This enables natural-language step definitions like Given a persistent callback is registered for the :on_boot hook.

## Implementation Notes ### Design Decisions 1. **HookEvent as frozen value object**: The `HookEvent` class freezes both itself and its `data` hash on initialization, ensuring immutability. This prevents handlers from accidentally mutating shared event state. 2. **LifecycleHooks as module with class-level methods**: Using `class << self` on a module gives us singleton behavior without requiring explicit instantiation. The `reset!` method supports test isolation. 3. **Mutex-based thread safety**: All callback storage mutations are protected by a `Mutex`. The `emit` method takes a snapshot (`.dup`) of the callback list before iteration to avoid holding the lock during callback execution, which prevents deadlocks if a callback registers/removes other callbacks. 4. **Block-identity-based `off`**: The `off` method matches callbacks by block object identity (`==`), consistent with the issue requirements. Callers must retain a reference to the original block to remove it. 5. **Extracted private methods for Rubocop compliance**: The `emit` method was refactored to delegate argument validation (`validate_emit_args!`), callback dispatch (`dispatch_callbacks`), and one-shot cleanup (`remove_once_entry`) to private methods, satisfying Metrics/AbcSize and Metrics/MethodLength constraints. ### Files Created | File | Purpose | |------|---------| | `lib/aethyr/core/components/lifecycle/hook_event.rb` | Immutable HookEvent data class | | `lib/aethyr/core/components/lifecycle/lifecycle_hooks.rb` | LifecycleHooks singleton registry | | `tests/unit/lifecycle_hooks.feature` | 20 Cucumber scenarios | | `tests/unit/step_definitions/lifecycle_hooks_steps.rb` | Step definitions with custom `:symbol` ParameterType | ### Test Results - **20 scenarios, 64 steps** — all passing - **Rubocop**: 0 offenses on new files - **Integration tests**: Pre-existing failure unrelated to this change (1 failed scenario existed before this branch) ### Custom ParameterType Added a `symbol` ParameterType to Cucumber for matching Ruby symbol literals (`:on_boot`, `:on_tick`, etc.) in Gherkin steps. This enables natural-language step definitions like `Given a persistent callback is registered for the :on_boot hook`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: aethyr/Aethyr#83
No description provided.