{"id":15713661,"url":"https://github.com/crystallabs/event_handler","last_synced_at":"2025-05-12T13:24:19.350Z","repository":{"id":62250416,"uuid":"221066242","full_name":"crystallabs/event_handler","owner":"crystallabs","description":"Application event model for Crystal","archived":false,"fork":false,"pushed_at":"2022-12-26T23:07:47.000Z","size":191,"stargazers_count":31,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-31T22:33:22.108Z","etag":null,"topics":["asynchronous","crystal","crystal-lang","emits-events","event-driven","event-emitter","event-handlers","eventemitter","events","synchronous"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/crystallabs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-11T20:40:46.000Z","updated_at":"2024-04-23T15:48:08.000Z","dependencies_parsed_at":"2023-01-31T02:15:36.910Z","dependency_job_id":null,"html_url":"https://github.com/crystallabs/event_handler","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fevent_handler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fevent_handler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fevent_handler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fevent_handler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/crystallabs","download_url":"https://codeload.github.com/crystallabs/event_handler/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253745744,"owners_count":21957436,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["asynchronous","crystal","crystal-lang","emits-events","event-driven","event-emitter","event-handlers","eventemitter","events","synchronous"],"created_at":"2024-10-03T21:32:46.223Z","updated_at":"2025-05-12T13:24:19.331Z","avatar_url":"https://github.com/crystallabs.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Linux CI](https://github.com/crystallabs/event_handler/workflows/Linux%20CI/badge.svg)](https://github.com/crystallabs/event_handler/actions?query=workflow%3A%22Linux+CI%22+event%3Apush+branch%3Amaster)\n[![Version](https://img.shields.io/github/tag/crystallabs/event_handler.svg?maxAge=360)](https://github.com/crystallabs/event_handler/releases/latest)\n[![License](https://img.shields.io/github/license/crystallabs/event_handler.svg)](https://github.com/crystallabs/event_handler/blob/master/LICENSE)\n\n# EventHandler\n\nEventHandler is a full-featured event library for Crystal.\n\nIt supports:\n\n1. Defining events\n1. Defining handlers that will run in response to events\n1. Emitting (triggering) events\n\nEach handler can run synchronously or asynchronously, run one or more\ntimes, and be added at the beginning or end of queue, or into a specific position.\n\nSubclassing events is also supported, as well as sending events through Channels\nand blocking/waiting for events.\n\n## Installation\n\nAdd the dependency to `shard.yml`:\n\n```yaml\ndependencies:\n  event_handler:\n    github: crystallabs/event_handler\n    version: ~\u003e 1.0\n```\n\n## Usage in a nutshell\n\nHere is a basic example that defines and emits events. More detailed usage instructions are provided further below.\n\n```crystal\nrequire \"event_handler\"\n\n# Define an event named ClickedEvent with two arguments\nEventHandler.event ClickedEvent, x : Int32, y : Int32\n\nclass MyClass\n  include EventHandler\n\n  def initialize\n    # Define a handler that will run in response to the event\n    on(::ClickedEvent) do |e|\n      puts \"Clicked on position x=#{e.x}, y=#{e.y}\"\n    end\n  end\nend\n\n# Trigger the event on the object:\nmy = MyClass.new\nmy.emit ClickedEvent, 10, 20 #=\u003e \"Clicked on position x=10, y=20\"\n```\n\nOr another example:\n\n```cr\nrequire \"event_handler\"\n\n# Define an event inside a namespace (MyClass::TestEvent)\nclass MyClass\n  include EventHandler\n  event TestEvent, message : String, status : Bool\nend\n\nmy = MyClass.new\n\n# Add a Proc as event handler\nhandler = -\u003e(e : MyClass::TestEvent) do\n  puts \"Activated on #{e.class}. Message is '#{e.message}' and status is #{e.status}\"\nend\nmy.on MyClass::TestEvent, handler\n\n# Emit the event\nmy.emit MyClass::TestEvent, \"Hello, World!\", true\n#=\u003e Activated on MyClass::TestEvent. Message is 'Hello, World!' and status is true\n\n# Remove the handler\nmy.off MyClass::TestEvent, handler\n\n# Or remove all handlers for an event at once\nmy.off MyClass::TestEvent\n```\n\n## Documentation\n\n### Defining events\n\nAn event can be defined via the convenient `event` macro or manually.\n\nUsing `event` creates an event class which inherits from base class `EventHandler::Event`:\n\n```crystal\nEventHandler.event ClickedEvent, x : Int32, y : Int32\n```\n\nIt is a shorthand for the following line:\n\n```crystal\nclass_record ClickedEvent \u003c ::EventHandler::Event, x : Int32, y : Int32\n```\n\n(`class_record` is EventHandler's variant of Crystal's macro `record`; it creates classes instead of structs.)\n\nIf additional modification to the class is necessary, class can be reopened:\n\n```crystal\nEventHandler.event ClickedEvent, x : Int32, y : Int32\n\nclass ClickedEvent \u003c EventHandler::Event\n  property test : String?\nend\n```\n\nOr the whole event class can be created manually; it only needs to inherit from `EventHandler::Event`:\n\n```crystal\nclass ClickedEvent \u003c EventHandler::Event\n  getter x : Int32\n  getter y : Int32\n  property test : String?\n  def initialize(@x, @y)\n  end\nend\n```\n\nSubclassing also works as expected:\n\n```crystal\nEventHandler.event ClickedEvent, x : Int32, y : Int32\n\nclass DoubleClickedEvent \u003c ClickedEvent\nend\n```\n\n### Adding event handlers\n\nEvent handlers can be added in a number of ways.\n\nUsing a block:\n\n```crystal\nmy = MyClass.new\n\nmy.on ClickedEvent do |e|\n  p \"Hello\"\nend\n```\n\nUsing a Proc:\n\n```crystal\nmy = MyClass.new\n\n# With Proc -\u003e(){} syntax\nhandler = -\u003e(e : ClickedEvent) do\n  p \"Hello\"\n  nil\nend\n\n# With Proc.new syntax\nhandler = Proc(ClickedEvent, Nil).new do |e|\n  p \"Hello\"\nend\n\nmy.on ClickedEvent, handler\n```\n\nUsing an aliased type for Proc called `Handler`, eliminating the need to repeat type information:\n\n```crystal\nmy = MyClass.new\n\nhandler = ClickedEvent::Handler.new do |e|\n  p \"Hello\"\nend\n\nmy.on ClickedEvent, handler\n```\n\nUsing an existing method:\n\n```crystal\nmy = MyClass.new\n\ndef on_clicked(e : ClickedEvent) : Nil\n  p \"Hello\"\nend\n\nmy.on ClickedEvent, -\u003eon_clicked(ClickedEvent)\n```\n\nUsing a variation of the last example, where if an object method is used, `self` is preserved as expected:\n\n```crystal\nclass MyClass\n  include EventHandler\n  event ClickedEvent, x : Int32, y : Int32\n\n  def on_clicked(e : ClickedEvent)\n    p \"Hello\", e.x, e.y, self\n    nil\n  end\nend\nmy = MyClass.new\n\nmy.on ClickedEvent, -\u003emy.on_clicked(ClickedEvent)\n```\n\nUsing a handler \"wrapper\" object explicitly (otherwise it would be created and used implicitly):\n\n```crystal\nmy = MyClass.new\n\nhandler = -\u003e(e : ClickedEvent) do\n  p \"Hello\"\n  nil\nend\nwrapper = EventHandler::Wrapper.new(handler: handler, once: false, async: false, at: -1)\n\nmy.on ClickedEvent, wrapper\n```\n\nUsing a variation of the last example with an aliased type for Wrapper:\n\n```crystal\nmy = MyClass.new\n\n# With block\nwrapper = ClickedEvent::Wrapper.new(once: false, async: false, at: -1) do |e|\n  p \"Hello\"\nend\n\n# With Proc\nhandler = -\u003e(e : ClickedEvent) do\n  p \"Hello\"\n  nil\nend\nwrapper = ClickedEvent::Wrapper.new(handler: handler, once: false, async: false, at: -1)\n\nmy.on ClickedEvent, wrapper\n```\n\nUsing a variation of the last example, where wrapper object is obtained from a call\nto `on()` and then reused to add the same handler the second time:\n\n```crystal\nmy = MyClass.new\n\nhandler = -\u003e(e : ClickedEvent) do\n  p \"Hello\"\n  nil\nend\n\nwrapper = my.on ClickedEvent, handler\n\nmy.on ClickedEvent, wrapper\n```\n\nUsing a Channel:\n\n```crystal\nmy = MyClass.new\n\n# With Channel(T)\nchannel = Channel(ClickedEvent).new\n\n# With an aliased type\nchannel = ClickedEvent::Channel.new\n\nmy.on ClickedEvent, channel\n```\n\nWhen `on` is invoked with a channel, it implicitly creates and\nadds an event handler which forwards received events into the channel.\n\n#### Event handler options\n\nAll of the above methods for adding handlers support arguments `once`, `async`, and `at`.\n\n`once` specifies whether the handler should run only once and then be automatically removed.\nDefault is false. In the future this option may be replaced with `times` which specifies\nhow many times to run before being removed.\n\nAs a convenience for adding handlers that should run only once, there is a method\nnamed `once` available instead of the usual `on`. These two calls are equivalent:\n\n```crystal\nmy.on ClickedEvent, handler, once: true, async: true, at: -1\n\nmy.once ClickedEvent, handler, async: true, at: -1\n```\n\n`async` specifies whether a handler should run synchronously or asynchronously. If\nno specific value is provided, global default from `EventHandler.async` is used.\nDefault (`EventHandler.async?`) is false. You can either modify this default,\nor specify `async` on a per-`on` basis.\n\n`at` specifies the index in the list of handlers where new handler should be inserted.\nWhile it is possible to specify the exact position, usually this value is\n`0` (`EventHandler.at_beginning`) to insert at the beginning or `-1` (`EventHandler.at_end`)\nto insert at the end of list. Default is `EventHandler.at_end`.\n\n### Emitting events\n\nEvents can be emitted using `emit` in one of three ways:\n\nBy listing the event class and arguments one after another:\n\n```crystal\nmy.emit ClickedEvent, 10, 20\n```\n\nBy listing the event class and event instance one after another:\n\n```crystal\nmy.emit ClickedEvent, ClickedEvent.new 10, 20\n```\n\nBy creating an event instance and providing it as the single argument:\n\n```crystal\nmy.emit ClickedEvent.new 10, 20\n```\n\nThe handler methods will always receive one argument - the event object\nwith packed arguments. The return value from `emit` is that object.\n\n### Handling events\n\nAs mentioned, handlers always receive one argument - the event object with packed arguments.\n\nWhen an event is emitted using any of the available variants, such as:\n\n```crystal\nmy.emit ClickedEvent, x: 10, y: 20\n```\n\nThe arguments are directly accessible as getters on the event object:\n\n```\nmy.on ClickedEvent do |e|\n  puts \"Clicked on position x=#{e.x}, y=#{e.y}\"\nend\n```\n\n### Return values\n\nAll handlers are defined with Nil as their return type and their return\nvalue is ignored.\n\n```crystal\nmy.on ClickedEvent do |e|\n  p \"Hello\"\nend\n```\n\nIf event handlers should produce a return value, the recommended way\nis to subclass Event into one that contains a return value, which\nthe handlers will update:\n\n```crystal\nrequire \"event_handler\"\n\nclass EventWithRetval \u003c ::EventHandler::Event\n  property return_value : Int32 = 0\nend\n\nclass_record ClickedEvent \u003c EventWithRetval, x : Int32, y : Int32\n\nclass MyClass\n  include ::EventHandler\nend\nc = MyClass.new\n\nc.on(ClickedEvent) { |e| e.return_value += e.x + e.y }\n\nevent = c.emit ClickedEvent, 1,2\np event.return_value #=\u003e 3\n\nc.emit ClickedEvent, event\np event.return_value #=\u003e 6\n\nc.emit event\np event.return_value #=\u003e 9\n```\n\nPlease note the above example will work correctly as long as event handlers are\ninvoked synchronously. Running one or more handlers asynchronously and checking\nfor the return value after all handlers have finished execution is currently not\naddressed as part of built-in functionality.\n\n### Inspecting event handlers\n\nTo inspect the current list of installed handlers for an event, use `handlers`:\n\n```crystal\nmy.handlers ClickedEvent\n\nmy.handlers(ClickedEvent).size\n\nmy.handlers(ClickedEvent).empty?\n```\n\nPlease note that `handlers` exposes the Array containing the list of handlers.\n\nModifying the array will directly modify the list of handlers defined for an event. This should only be done with due caution.\n\n### Removing event handlers\n\nEvent handlers can be removed in one of five ways:\n\nBy handler Proc:\n\n```crystal\nhandler = ClickedEvent::Handler.new do |e|\n  p \"Hello\"\nend\n\nmy.on ClickedEvent, handler\nmy.off ClickedEvent, handler\n```\n\nBy handler hash:\n\n```crystal\nhandler = ClickedEvent::Handler.new do |e|\n  p \"Hello\"\nend\n\nhash = handler.hash\n\nmy.on ClickedEvent, handler\nmy.off ClickedEvent, hash\n```\n\nBy handler wrapper object:\n\n```crystal\nhandler = ClickedEvent::Handler.new {\n  p \"Hello\"\n}\n\nwrapper = my.on ClickedEvent, handler\nmy.off ClickedEvent, wrapper\n```\n\nInternally, handlers are always removed from events by removing their wrapper\nobject.\n\nWhen wrappers are created implicitly by `on`, each invocation of `on`\ngives handler a new wrapper object even if the same handler is added multiple\ntimes for the same event. A call to\n`off()` will find the first instance of this handler, then remove all\ninstances of its wrapper from the list (there will be only one), and then\ninvoke `RemoveHandlerEvent` with that instance as argument.\nIf a handler is added to an event more than once, it will be necessary to call\n`off()` multiple times to remove all instances.\n\nWhen handlers are added by using their wrappers directly, multiple identical\nwrapper objects will be present in the list.\nWhen `off()` is used to remove such handlers, all instances of their wrapper\nwill be removed from the list (there will be more than one) and `RemoveHandlerEvent`\nwill be invoked with the last removed instance as argument.\n\nWhether `off(Event, handler | hash)` should be removing handlers by\nwrapper (like it does now) or by handler, and whether `off()` should remove\nall instances (like it does now) or at most one, is still being considered.\n\nBy handler index in the `handlers` Array:\n\n```crystal\nmy.off ClickedEvent, at: 0\n```\n\nBy removing all handlers at once:\n\n```crystal\n# With off\nmy.off ClickedEvent\n\n# With remove_all_handlers\nmy.remove_all_handlers ClickedEvent\n```\n\nWhen all handlers are removed at once, `RemoveHandlerEvent`s will be emitted as\nexpected, and any multiple identical wrappers will be removed according to the\nabove-documented behavior.\nIf emitting `RemoveHandlerEvent` events should be disabled when removing all handlers,\nprovide argument *emit* to `off` or `remove_all_handlers`, or use\n`EventHandler.emit_on_remove_all?` and `EventHandler.emit_on_remove_all=`\nto change the default behavior.\n\n### Meta Events\n\nThere are three built-in events:\n\n`AddHandlerEvent` - Event emitted after a handler is added for any event, including itself.\n\n`RemoveHandlerEvent` - Event emitted after a handler is removed from any event, including itself.\n\n`AnyEvent` - Event emitted on any event. Adding a handler for this event allows listening for all emitted events and their arguments.\n\nAs mentioned, a wrapper object is implicitly created around a handler on every `on`, to encapsulate the handler and its\nsubscription options (the values of `once?`, `async?`, and `at`).\nWhen `AddHandlerEvent` or `RemoveHandlerEvent` are emitted, they are invoked with the\nhandlers' `Wrapper` object as argument.\nThis allows listeners on these two meta events full insight into the added or removed handlers and their subscription data.\n\n### Channels\n\nEmitted events can also be sent through Channels. EventHandler comes with\nconvenience types and functions for this purpose:\n\nChannels can be created with Channel(T) or an aliased type:\n\n```crystal\n# With Channel(T)\nchannel = Channel(ClickedEvent).new\n\n# With an aliased type\nchannel = ClickedEvent::Channel.new\n```\n\nInvoking `on` with a Channel argument will implicitly create a handler that\nforwards emitted events to the Channel:\n\n```crystal\nmy.on ClickedEvent, channel, async: true\n```\n\nThe same behavior can also be implemented manually:\n\n```crystal\nchannel = Channel(ClickedEvent).new\n\nmy.on ClickedEvent, async: true do |e|\n  channel.send e\nend\n```\n\nA complete example:\n\n```crystal\nrequire \"event_handler\"\n\nEventHandler.event ClickedEvent, x : Int32, y : Int32\n\nclass My\n  include EventHandler\nend\nmy = My.new\n\n# Create a channel, wait for event, and print it\nchannel = ClickedEvent::Channel.new\nmy.once ClickedEvent, channel, async: true\nmy.emit(ClickedEvent, 1,2)\np channel.receive\n\n# Same as above, implemented manually\nchannel = Channel(ClickedEvent).new\nmy.once ClickedEvent, async: true do |e|\n  channel.send e\nend\nmy.emit(ClickedEvent, 1,2)\np channel.receive\n```\n\n### Waiting for events\n\nUsing Channels, it is also possible to wait for events.\n\nThe above example already shows blocking on `channel.receive`.\nThe same effect can be achieved using convenience method `wait` and\navoiding visible use of Channels:\n\n```crystal\ne = my.wait(ClickedEvent)\n```\n\n`wait` can also be invoked with code. The accepted syntax and\narguments are the same as for `once`:\n\n```crystal\n# With a block\nmy.wait ClickedEvent do |e|\n  p \"Hello\"\nend\n\n# With a Proc\nhandler = ClickedEvent::Handler.new do |e|\n  p \"Hello\"\nend\nmy.wait ClickedEvent, handler\n\n# With a method\ndef on_clicked(e : ClickedEvent) : Nil\n  p \"Hello\"\nend\nmy.wait(ClickedEvent, -\u003eon_clicked(ClickedEvent))\n```\n\nWhen waiting for events with code, two handlers are involved:\n\nThe first, visible one is the handler provided to\n`wait`, containing code to execute once the event arrives.\n`wait` argument *async* controls whether this handler will\nrun synchronously or asynchronously after the event has been\nwaited. This is consistent with the usual behavior and the\ndefault value is `false` (`EventHandler.async?`).\n\nThe other, implicit one is the handler automatically created\nand added to the list of event handlers. Once the event is\nemitted and this handler runs, it will forward the received\nevent into the Channel.\n`wait` argument *async_send* controls whether the event emitter\nwill block on `channel.send` or it will execute it in\na new fiber. The default value is `false` (`EventHandler.async_send?`).\n\n### Subclassing\n\nEvent classes can be subclassed with no restrictions:\n\n```crystal\nrequire \"event_handler\"\n\nEventHandler.event ClickedEvent, x : Int32, y : Int32\n\nclass DoubleClickedEvent \u003c ClickedEvent\nend\n\nclass TripleClickedEvent \u003c DoubleClickedEvent\n  def initialize(@x : Int32, @y : Int32)\n    @z = 0\n  end\n\n  def initialize(@x : Int32, @y : Int32, @z : Int32)\n  end\nend\n\nclass My\n  include EventHandler\n\n  def initialize\n    on(ClickedEvent)       {|e| p e }\n    on(DoubleClickedEvent) {|e| p e }\n    on(TripleClickedEvent) {|e| p e }\n  end\nend\n\nmy = My.new\nmy.emit ClickedEvent, 1, 2\nmy.emit DoubleClickedEvent, 3, 4\nmy.emit TripleClickedEvent, 5, 6\nmy.emit TripleClickedEvent, 7, 8, 9\n```\n\nHere is an example of an Event subclass that counts the number of times\nthe event was instantiated:\n\n```crystal\nrequire \"event_handler\"\n\nabstract class EventWithCount \u003c ::EventHandler::Event\n  class_property count : UInt64 = 0\n\n  def initialize\n    @@count += 1\n  end\nend\n\nclass ClickedEvent \u003c EventWithCount\n  getter x : Int32\n  getter y : Int32\n  def initialize(@x, @y)\n    super()\n  end\nend\n\nclass My; include EventHandler end\nmy = My.new\n\n4.times { my.emit ClickedEvent, 1, 2 }\n\np ClickedEvent.count #=\u003e 4\n```\n\n## API documentation\n\nRun `crystal docs` as usual, then open file `docs/index.html`.\n\nAlso, see examples in the directory `examples/`.\n\n## Testing\n\nRun `crystal spec` as usual.\n\nAlso, see examples in the directory `examples/`.\n\n## Thanks\n\n* All the fine folks on Libera.Chat IRC channel #crystal-lang and on Crystal's Gitter channel https://gitter.im/crystal-lang/crystal\n\n* Blacksmoke16 for a workable event model design\n\n* Asterite, Absolutejam, and Tenebrousedge for additional discussion\n\n## Other projects\n\nList of interesting or similar projects in no particular order:\n\n- https://github.com/Papierkorb/cute - Event-centric pub/sub model for objects inspired by the Qt framework\n\n- https://github.com/hugoabonizio/event_emitter.cr - Idiomatic asynchronous event-driven architecture\n\n- https://github.com/vladfaust/callbacks.cr - Expressive callbacks module for Crystal\n\n- https://github.com/anykeyh/await_async - Provide await and async methods to Crystal\n\n- https://github.com/firejox/CrSignals - Signals/slots notification library in Crystal\n\n- https://github.com/crystal-community/future.cr - Provides delay, future, and lazy convenient methods\n\n## Licensing\n\nFor licensing to use in your next project, consider\nhttps://perens.com/2020/10/06/post-open-source-license-early-draft/ and https://licenseuse.org/.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrystallabs%2Fevent_handler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrystallabs%2Fevent_handler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrystallabs%2Fevent_handler/lists"}