metacosm
Description
Metacosm is an awesome microframework for building reactive systems.
The idea is to enable quick prototyping of command-query separated or event-sourced systems.
One core concept is that we use commands to update "write-only" models, which trigger events that update "read-only" view models that are used by queries.
Models only transform their state in response to commands, so their state can be reconstructed by replaying the stream of commands.
Features
- One interesting feature here is a sort of mock in-memory AR component called
Registrablethat is used for internal tests (note: this has been extracted to PassiveRecord)
Examples
Below is a Fizzbuzz implementation contrived enough to show off many of the features of the framework.
You may also want to look at this repository which implements Conway's game of life using metacosm and gosu.
require 'metacosm'
include Metacosm
class Counter < Model
def initialize
@counter = 0
super
end
def fizz!
emit fizz
end
def buzz!
emit buzz
end
def increment!(inc)
@counter += inc
emit(counter_incremented)
end
protected
def fizz
FizzEvent.create
end
def buzz
BuzzEvent.create
end
def counter_incremented
CounterIncrementedEvent.create(
value: @counter,
counter_id: @id
)
end
end
class CounterView < View
attr_accessor :value, :counter_id
def update_value(new_value)
@value = new_value
self
end
end
class IncrementCounterCommand < Command
attr_accessor :increment, :counter_id
end
class IncrementCounterCommandHandler
def handle(increment:,counter_id:)
counter = Counter.find(counter_id)
counter.increment!(increment)
end
end
class CounterIncrementedEvent < Event
attr_accessor :value, :counter_id
end
class CounterIncrementedEventListener < EventListener
def receive(value:,counter_id:)
update_counter_view(counter_id, value)
fizz_buzz!(counter_id, value)
puts(value) unless fizz?(value) || buzz?(value)
end
def update_counter_view(counter_id, value)
counter_view = CounterView.where(counter_id: counter_id).first_or_create
counter_view.update value: value
end
private
def fizz_buzz!(counter_id, n)
fire(FizzCommand.create(counter_id: counter_id)) if fizz?(n)
fire(BuzzCommand.create(counter_id: counter_id)) if buzz?(n)
end
def fizz?(n); n % 3 == 0 end
def buzz?(n); n % 5 == 0 end
end
class FizzCommand < Command
attr_accessor :counter_id
end
class FizzCommandHandler
def handle(counter_id:)
counter = Counter.find(counter_id)
counter.fizz!
end
end
class BuzzCommand < Command
attr_accessor :counter_id
end
class BuzzCommandHandler
def handle(counter_id:)
counter = Counter.find(counter_id)
counter.buzz!
end
end
class FizzEvent < Event
end
class FizzEventListener < EventListener
def receive
puts "fizz"
end
end
class BuzzEvent < Event
end
class BuzzEventListener < EventListener
def receive
puts "buzz"
end
end
Given all this prelude we can run a fizzbuzz "simulation":
sim = Simulation.current
counter_model = Counter.create
counter_view = CounterView.find_by(counter_id: counter_model.id)
counter_view.value # => 0
increment_counter_command = IncrementCounterCommand.create(
increment: 1, counter_id: counter_model.id
)
sim.apply(increment_counter_command)
counter_view.value # => 1
100.times { sim.apply(increment_counter_command) }
sim.events.take(10)
# => [CounterCreatedEvent (id: 1, counter_id: 1),
# CounterIncrementedEvent (id: 1, value: 1, counter_id: 1),
# CounterIncrementedEvent (id: 2, value: 2, counter_id: 1),
# CounterIncrementedEvent (id: 3, value: 3, counter_id: 1),
# FizzEvent (id: 1),
# CounterIncrementedEvent (id: 4, value: 4, counter_id: 1),
# CounterIncrementedEvent (id: 5, value: 5, counter_id: 1),
# BuzzEvent (id: 1),
# CounterIncrementedEvent (id: 6, value: 6, counter_id: 1)]
Requirements
Install
$ gem install metacosm
Synopsis
$ metacosm
Copyright
Copyright (c) 2016 Joseph Weissman
See LICENSE for details.