A small Ruby gem for defining an immutable collection of named callbacks.
CallbackCollection groups callback definitions during initialization and
exposes a single respond_with interface for invoking them. The registry
becomes immutable once constructed, preventing late additions and making its
behavior easier to test and reason about.
The gem does not replace framework callbacks or start threads or Ractors itself. It provides an independent container that applications can use within their own execution model.
- Immutable after initialization: the collection and its internal registry are frozen when the configuration block completes. Callback definitions can then be read concurrently by multiple threads without mutating shared registry state.
- Optional Ractor compatibility: on Ruby 3.0 and later,
registerstores a receiver and method name instead of a closure. The collection is shareable when the receiver and all the state it uses are also Ractor-shareable. - Ruby 2.7 and later: the standard API and
registerdo not depend onRactor. Applications running Ruby 2.7 retain the immutable registry and concurrent thread-read behavior. - Direct lookup:
respond_withusesHash#fetchto find a callback in one operation and raises an explicit error when it does not exist.
Immutability protects the registry, not automatically the code executed by its callbacks. A callback that accesses mutable shared state remains responsible for its own synchronization.
Install the gem from RubyGems:
gem install callback-collectionWith Bundler, add it to your Gemfile:
gem "callback-collection"require "callback_collection"
callbacks = CallbackCollection.new do |collection|
collection.success { |name| "Welcome, #{name}!" }
collection.failure { |error| "Error: #{error.message}" }
end
callbacks.respond_with(:success, "Ruby")
# => "Welcome, Ruby!"The collection is frozen at the end of initialization. Attempting to add a
callback afterward raises FrozenError.
Blocks retain their lexical context and therefore cannot be shared between Ractors. To create a shareable collection, register a shareable receiver and one of its methods instead:
module Handlers
def self.sum(left, right)
left + right
end
end
callbacks = CallbackCollection.new do |collection|
collection.register(:sum, Handlers)
end
Ractor.shareable?(callbacks)
# => true
worker = Ractor.new(callbacks) do |collection|
collection.respond_with(:sum, 20, 22)
end
result = worker.respond_to?(:value) ? worker.value : worker.take
result
# => 42The third argument to register lets you use a method name that differs from
the callback name:
collection.register(:total, Handlers, :sum)The registered receiver and the data it uses must follow Ractor shareability
rules. Calls to respond_with remain unchanged. On Ruby 2.7, the gem and
register remain fully usable; only Ractor execution is unavailable. The gem
does not create workers: the application retains control over their lifecycle
and exchanged messages.
bundle install
bundle exec rakeThe default task runs the Minitest suite and builds the gem in pkg/. GitHub
Actions checks the project with Ruby 2.7 through 3.4 and the latest stable
release, Ruby 4.0.
RubyGems releases use Trusted Publishing and do not require an API key in GitHub secrets.
Before the first release, create a Pending Trusted Publisher in your RubyGems profile with these settings:
- gem:
callback-collection - repository owner:
nicolasva - repository:
callback-collection - workflow:
release.yml - GitHub environment:
release
Release a version by pushing the tag that matches
CallbackCollection::VERSION:
VERSION=$(ruby -Ilib -rcallback_collection/version -e 'print CallbackCollection::VERSION')
git tag "v${VERSION}"
git push origin "v${VERSION}"GitHub Actions then builds and publishes the gem. RubyDoc generates its documentation automatically, and the version and download badges update after publication.