akasha 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +14 -1
- data/README.md +6 -4
- data/akasha.gemspec +2 -0
- data/examples/sinatra/Gemfile +4 -0
- data/examples/sinatra/Gemfile.lock +24 -0
- data/examples/sinatra/app.rb +67 -0
- data/lib/akasha.rb +3 -0
- data/lib/akasha/event.rb +6 -0
- data/lib/akasha/event_listener.rb +5 -0
- data/lib/akasha/event_router.rb +38 -0
- data/lib/akasha/repository.rb +30 -2
- data/lib/akasha/storage/http_event_store.rb +24 -0
- data/lib/akasha/storage/http_event_store/stream.rb +50 -0
- data/lib/akasha/storage/memory_event_store/stream.rb +6 -7
- data/lib/akasha/version.rb +1 -1
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '09372b1685568c249f091ff1bbbf4915bb574b91'
|
4
|
+
data.tar.gz: 982ef7993d2087b2bd55f424228ba5bdf31d867a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59730ad9b8f01e86d5fdfd01945fea0586002972571595f03810d7af600f9d864a5d5c22fc1e7092a68067f6fd310c16f825980b8515d71f7933a05f2362dfc1
|
7
|
+
data.tar.gz: 1fb02d4091efe00a925af1ec4d15990c0cc0c61aaaf64a1c64de36a63dcc3eea3b73465d5e558880ad46393d90146dfb6a788d5e5544f7afcdf68bda67f38d78
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
akasha (0.
|
4
|
+
akasha (0.2.0)
|
5
|
+
http_event_store
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -9,7 +10,19 @@ GEM
|
|
9
10
|
byebug (10.0.2)
|
10
11
|
coderay (1.1.2)
|
11
12
|
diff-lcs (1.3)
|
13
|
+
faraday (0.15.2)
|
14
|
+
multipart-post (>= 1.2, < 3)
|
15
|
+
faraday_middleware (0.12.2)
|
16
|
+
faraday (>= 0.7.4, < 1.0)
|
17
|
+
hashie (3.5.7)
|
18
|
+
http_event_store (0.2.2)
|
19
|
+
faraday
|
20
|
+
faraday_middleware
|
21
|
+
hashie
|
22
|
+
json
|
23
|
+
json (2.1.0)
|
12
24
|
method_source (0.9.0)
|
25
|
+
multipart-post (2.0.0)
|
13
26
|
pry (0.11.3)
|
14
27
|
coderay (~> 1.1.0)
|
15
28
|
method_source (~> 0.9.0)
|
data/README.md
CHANGED
@@ -29,7 +29,7 @@ require 'sinatra'
|
|
29
29
|
|
30
30
|
class User < Akasha::Aggregate
|
31
31
|
def sign_up(email:, password:, admin: false, **)
|
32
|
-
changeset
|
32
|
+
changeset.append(:user_signed_up, email: email, password: password, admin: admin)
|
33
33
|
end
|
34
34
|
|
35
35
|
def on_user_signed_up(email:, password:, admin:, **)
|
@@ -73,11 +73,13 @@ end
|
|
73
73
|
## Next steps
|
74
74
|
|
75
75
|
- [x] Command routing (default and user-defined)
|
76
|
-
- [
|
77
|
-
- [
|
76
|
+
- [x] Synchronous EventHandler
|
77
|
+
- [x] HTTP Eventstore storage backend
|
78
|
+
- [ ] Telemetry (Dogstatsd)
|
79
|
+
- [ ] Event#id for better idempotence (validate this claim)
|
78
80
|
- [ ] Namespacing for events and aggregates
|
79
81
|
- [ ] Version-based concurrency
|
80
|
-
- [ ]
|
82
|
+
- [ ] Async EventHandlers (storing cursors in Eventstore, configurable durability guarantees)
|
81
83
|
- [ ] Socket-based Eventstore storage backend
|
82
84
|
|
83
85
|
## Development
|
data/akasha.gemspec
CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
24
|
spec.require_paths = ['lib']
|
25
25
|
|
26
|
+
spec.add_dependency 'http_event_store'
|
27
|
+
|
26
28
|
spec.add_development_dependency 'bundler', '~> 1.16'
|
27
29
|
spec.add_development_dependency 'rake', '~> 10.0'
|
28
30
|
spec.add_development_dependency 'rspec', '~> 3.7'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
akasha (0.1.0)
|
5
|
+
mustermann (1.0.2)
|
6
|
+
rack (2.0.5)
|
7
|
+
rack-protection (2.0.1)
|
8
|
+
rack
|
9
|
+
sinatra (2.0.1)
|
10
|
+
mustermann (~> 1.0)
|
11
|
+
rack (~> 2.0)
|
12
|
+
rack-protection (= 2.0.1)
|
13
|
+
tilt (~> 2.0)
|
14
|
+
tilt (2.0.8)
|
15
|
+
|
16
|
+
PLATFORMS
|
17
|
+
ruby
|
18
|
+
|
19
|
+
DEPENDENCIES
|
20
|
+
akasha
|
21
|
+
sinatra
|
22
|
+
|
23
|
+
BUNDLED WITH
|
24
|
+
1.16.2
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'akasha'
|
2
|
+
require 'sinatra'
|
3
|
+
|
4
|
+
class User < Akasha::Aggregate
|
5
|
+
attr_reader :email
|
6
|
+
|
7
|
+
def sign_up(email:, password:, admin: false, **)
|
8
|
+
changeset.append(:user_signed_up, email: email, password: password, admin: admin)
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_user_signed_up(email:, password:, admin:, **)
|
12
|
+
@email = email
|
13
|
+
@password = password
|
14
|
+
@admin = admin
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Notifier < Akasha::EventListener
|
19
|
+
def on_user_signed_up(user_id, **)
|
20
|
+
notify_about_signup(user_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def notify_about_signup(user_id)
|
26
|
+
# Here we could just grab email from the event but let's demonstrate
|
27
|
+
# how to load an aggregate from events.
|
28
|
+
user = User.find_or_create(user_id)
|
29
|
+
email = <<~EOS
|
30
|
+
User #{user.email} just signed up!
|
31
|
+
EOS
|
32
|
+
# Let's not send any emails... :)
|
33
|
+
puts email
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
@command_router = Akasha::CommandRouter.new
|
39
|
+
|
40
|
+
# Aggregates will load from and save to in-memory storage.
|
41
|
+
repository = Akasha::Repository.new(Akasha::Storage::MemoryEventStore.new)
|
42
|
+
Akasha::Aggregate.connect!(repository)
|
43
|
+
|
44
|
+
# Set up event listeners.
|
45
|
+
@event_router = Akasha::EventRouter.new
|
46
|
+
@event_router.register_event_listener(:user_signed_up, Notifier)
|
47
|
+
repository.subscribe(@event_router)
|
48
|
+
|
49
|
+
# This is how you link commands to aggregates.
|
50
|
+
@command_router.register_default_route(:sign_up, User)
|
51
|
+
|
52
|
+
# Nearly identital to the default handling above but we're setting the admin
|
53
|
+
# flag to demo custom command handling.
|
54
|
+
@command_router.register_route(:sign_up_admin) do |aggregate_id, **data|
|
55
|
+
user = User.find_or_create(aggregate_id)
|
56
|
+
user.sign_up(email: data[:email], password: data[:password], admin: true)
|
57
|
+
user.save!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
post '/users/:user_id' do # With CQRS client pass unique aggregate ids.
|
62
|
+
@command_router.route!(:sign_up,
|
63
|
+
params[:user_id],
|
64
|
+
email: params[:email],
|
65
|
+
password: params[:password])
|
66
|
+
'OK'
|
67
|
+
end
|
data/lib/akasha.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'akasha/event'
|
2
2
|
require 'akasha/aggregate'
|
3
3
|
require 'akasha/command_router'
|
4
|
+
require 'akasha/event_listener'
|
5
|
+
require 'akasha/event_router'
|
4
6
|
require 'akasha/repository'
|
7
|
+
require 'akasha/storage/http_event_store'
|
5
8
|
require 'akasha/storage/memory_event_store'
|
data/lib/akasha/event.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Akasha
|
2
|
+
# Routes events to event listeners.
|
3
|
+
class EventRouter
|
4
|
+
def initialize
|
5
|
+
@routes = Hash.new { |hash, key| hash[key] = [] }
|
6
|
+
end
|
7
|
+
|
8
|
+
# Registers a new event listener, derived from
|
9
|
+
# `Akasha::EventListener`.
|
10
|
+
def register_event_listener(event_name, listener_class)
|
11
|
+
@routes[event_name] << listener_class
|
12
|
+
end
|
13
|
+
|
14
|
+
# Routes an event.
|
15
|
+
def route(event_name, aggregate_id, **data)
|
16
|
+
@routes[event_name].each do |listener_class|
|
17
|
+
listener = listener_class.new
|
18
|
+
begin
|
19
|
+
listener.public_send(:"on_#{event_name}", aggregate_id, **data)
|
20
|
+
rescue RuntimeError => e
|
21
|
+
log "Error handling event #{event_name.inspect}: #{e}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Routes an event (an Akasha::Event instance).
|
27
|
+
# This is interface allowing subscription via `Akasha::Repository#subscribe`.
|
28
|
+
def call(aggregate_id, event)
|
29
|
+
route(event.name, aggregate_id, **event.data)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def log(msg)
|
35
|
+
puts msg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/akasha/repository.rb
CHANGED
@@ -5,25 +5,43 @@ module Akasha
|
|
5
5
|
class Repository
|
6
6
|
STREAM_NAME_SEP = '-'.freeze
|
7
7
|
|
8
|
+
# Creates a new repository using the underlying `store` (e.g. `MemoryEventStore`).
|
8
9
|
def initialize(store)
|
9
10
|
@store = store
|
11
|
+
@subscribers = []
|
10
12
|
end
|
11
13
|
|
14
|
+
# Loads an aggregate identified by `id` and `klass` from the repository.
|
15
|
+
# Returns an aggregate instance of class `klass` constructed by applying events from the corresponding
|
16
|
+
# stream.
|
12
17
|
def load_aggregate(klass, id)
|
13
18
|
agg = klass.new(id)
|
14
19
|
|
15
20
|
start = 0
|
16
|
-
|
17
|
-
stream(klass, id).read_events(start,
|
21
|
+
page_size = 20
|
22
|
+
stream(klass, id).read_events(start, page_size) do |events|
|
18
23
|
agg.apply_events(events)
|
19
24
|
end
|
20
25
|
|
21
26
|
agg
|
22
27
|
end
|
23
28
|
|
29
|
+
# Saves an aggregate to the repository, appending events to the corresponding stream.
|
24
30
|
def save_aggregate(aggregate)
|
25
31
|
changeset = aggregate.changeset
|
26
32
|
stream(aggregate.class, changeset.aggregate_id).write_events(changeset.events)
|
33
|
+
notify_subscribers(aggregate)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Subscribes to event streams passing either a lambda or a block.
|
37
|
+
# Example:
|
38
|
+
#
|
39
|
+
# repo.subscribe do |aggregate_id, event|
|
40
|
+
# ... handle the event ...
|
41
|
+
# end
|
42
|
+
def subscribe(lambda = nil, &block)
|
43
|
+
callable = lambda || block
|
44
|
+
@subscribers << callable
|
27
45
|
end
|
28
46
|
|
29
47
|
private
|
@@ -35,5 +53,15 @@ module Akasha
|
|
35
53
|
def stream(aggregate_klass, aggregate_id)
|
36
54
|
@store.streams[stream_name(aggregate_klass, aggregate_id)]
|
37
55
|
end
|
56
|
+
|
57
|
+
def notify_subscribers(aggregate)
|
58
|
+
id = aggregate.changeset.aggregate_id
|
59
|
+
events = aggregate.changeset.events
|
60
|
+
@subscribers.each do |subscriber|
|
61
|
+
events.each do |event|
|
62
|
+
subscriber.call(id, event)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
38
66
|
end
|
39
67
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'http_event_store/stream'
|
2
|
+
require 'http_event_store'
|
3
|
+
|
4
|
+
module Akasha
|
5
|
+
module Storage
|
6
|
+
# HTTP-based interface to Eventstore (https://geteventstore.com)
|
7
|
+
class HttpEventStore
|
8
|
+
def initialize(host: 'localhost', port: 2113)
|
9
|
+
@client = ::HttpEventStore::Connection.new do |config|
|
10
|
+
config.endpoint = host
|
11
|
+
config.port = port
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def streams
|
16
|
+
self # Use the `[]` method on self.
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](stream_name)
|
20
|
+
Stream.new(@client, stream_name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Akasha
|
2
|
+
module Storage
|
3
|
+
class HttpEventStore
|
4
|
+
# HTTP Eventstore stream.
|
5
|
+
class Stream
|
6
|
+
def initialize(client, stream_name)
|
7
|
+
@client = client
|
8
|
+
@stream_name = stream_name
|
9
|
+
end
|
10
|
+
|
11
|
+
# Appends events to the stream.
|
12
|
+
def write_events(events)
|
13
|
+
event_hashes = events.map do |event|
|
14
|
+
{
|
15
|
+
event_type: event.name,
|
16
|
+
data: event.data,
|
17
|
+
metadata: event.metadata
|
18
|
+
}
|
19
|
+
end
|
20
|
+
@client.append_to_stream(@stream_name, event_hashes)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Reads events from the stream starting from `start` inclusive.
|
24
|
+
# If block given, reads all events from the position in pages of `page_size`.
|
25
|
+
# If block not given, reads `size` events from the position.
|
26
|
+
def read_events(start, page_size)
|
27
|
+
if block_given?
|
28
|
+
position = start
|
29
|
+
loop do
|
30
|
+
events = read_events(position, page_size)
|
31
|
+
return if events.empty?
|
32
|
+
yield(events)
|
33
|
+
position += events.size
|
34
|
+
end
|
35
|
+
else
|
36
|
+
safe_read_events(start, page_size)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def safe_read_events(start, page_size)
|
43
|
+
@client.read_events_forward(@stream_name, start, page_size)
|
44
|
+
rescue ::HttpEventStore::StreamNotFound
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -12,15 +12,14 @@ module Akasha
|
|
12
12
|
@events += events
|
13
13
|
end
|
14
14
|
|
15
|
-
# Reads events from the stream starting from `
|
16
|
-
# If block given, reads all events from the
|
17
|
-
#
|
18
|
-
|
19
|
-
def read_events(position, size, &block)
|
15
|
+
# Reads events from the stream starting from `start` inclusive.
|
16
|
+
# If block given, reads all events from the start in pages of `page_size`.
|
17
|
+
# If block not given, reads `page_size` events from the start.
|
18
|
+
def read_events(start, page_size, &block)
|
20
19
|
if block_given?
|
21
|
-
@events.lazy.drop(
|
20
|
+
@events.lazy.drop(start).each_slice(page_size, &block)
|
22
21
|
else
|
23
|
-
@events[
|
22
|
+
@events[start..start + page_size]
|
24
23
|
end
|
25
24
|
end
|
26
25
|
end
|
data/lib/akasha/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: akasha
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcin Bilski
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-06-
|
11
|
+
date: 2018-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http_event_store
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,6 +115,9 @@ files:
|
|
101
115
|
- akasha.gemspec
|
102
116
|
- bin/console
|
103
117
|
- bin/setup
|
118
|
+
- examples/sinatra/Gemfile
|
119
|
+
- examples/sinatra/Gemfile.lock
|
120
|
+
- examples/sinatra/app.rb
|
104
121
|
- lib/akasha.rb
|
105
122
|
- lib/akasha/aggregate.rb
|
106
123
|
- lib/akasha/aggregate/syntax_helpers.rb
|
@@ -108,7 +125,11 @@ files:
|
|
108
125
|
- lib/akasha/command_router.rb
|
109
126
|
- lib/akasha/command_router/default_handler.rb
|
110
127
|
- lib/akasha/event.rb
|
128
|
+
- lib/akasha/event_listener.rb
|
129
|
+
- lib/akasha/event_router.rb
|
111
130
|
- lib/akasha/repository.rb
|
131
|
+
- lib/akasha/storage/http_event_store.rb
|
132
|
+
- lib/akasha/storage/http_event_store/stream.rb
|
112
133
|
- lib/akasha/storage/memory_event_store.rb
|
113
134
|
- lib/akasha/storage/memory_event_store/stream.rb
|
114
135
|
- lib/akasha/version.rb
|