entity_store 1.2.0 → 1.5.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/lib/entity_store/config.rb +9 -8
- data/lib/entity_store/event_bus.rb +49 -11
- data/lib/entity_store/not_found.rb +2 -2
- data/lib/entity_store/store.rb +55 -8
- data/lib/entity_store/version.rb +1 -1
- data/lib/entity_store.rb +0 -2
- data/spec/entity_store/entity_spec.rb +1 -1
- data/spec/entity_store/store_spec.rb +50 -2
- data/spec/entity_store_spec.rb +54 -1
- metadata +3 -5
- data/lib/entity_store/utils.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4575519de291e09a6f02085a07daeb09818edc01a5d8895204d443fc03db6b9
|
4
|
+
data.tar.gz: ce27f46beb087f7bcd48b6d5e690db1239e75adaffdcb524b1c24df0cbeea49f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de737107e012f7a07b5aea26280b425a6db374e13325054344e7c9fac196ee98bde2dbcc7d231fc0cd28b26992d805188a06d727373ef7c46e3552598c9c6e52
|
7
|
+
data.tar.gz: a624bb070c990b68b0a436d07c940f656238a244a80bfe2c789f3bb7feb4f8de0e66c65696496d75b936557e5bb4465466cd1e5f05f1b4583e8cf3bab3cc31ee
|
data/lib/entity_store/config.rb
CHANGED
@@ -4,9 +4,11 @@ module EntityStore
|
|
4
4
|
# Stores
|
5
5
|
attr_accessor :store, :feed_store
|
6
6
|
|
7
|
+
attr_accessor :cache_event_subscribers
|
8
|
+
|
7
9
|
# Allows config to pass in a lambda or Proc to use as the type loader in place
|
8
|
-
# of the default.
|
9
|
-
# Original use case was migration of entity classes to new module namespace when
|
10
|
+
# of the default.
|
11
|
+
# Original use case was migration of entity classes to new module namespace when
|
10
12
|
# extracting to a shared library
|
11
13
|
attr_accessor :type_loader
|
12
14
|
|
@@ -17,15 +19,15 @@ module EntityStore
|
|
17
19
|
yield self
|
18
20
|
|
19
21
|
raise StandardError.new("store not assigned") unless store
|
20
|
-
store.open
|
22
|
+
store.open
|
21
23
|
feed_store.open if feed_store
|
22
24
|
end
|
23
|
-
|
25
|
+
|
24
26
|
def event_subscribers
|
25
27
|
@_event_subscribers ||=[]
|
26
28
|
end
|
27
|
-
|
28
|
-
# Public - indicates the version increment that is used to
|
29
|
+
|
30
|
+
# Public - indicates the version increment that is used to
|
29
31
|
# decided whether a snapshot of an entity should be created when it's saved
|
30
32
|
def snapshot_threshold
|
31
33
|
@_snapshot_threshold ||= 10
|
@@ -35,7 +37,6 @@ module EntityStore
|
|
35
37
|
@_snapshot_threshold = value
|
36
38
|
end
|
37
39
|
|
38
|
-
|
39
40
|
def load_type(type_name)
|
40
41
|
if EntityStore::Config.type_loader
|
41
42
|
EntityStore::Config.type_loader.call(type_name)
|
@@ -50,4 +51,4 @@ module EntityStore
|
|
50
51
|
end
|
51
52
|
|
52
53
|
end
|
53
|
-
end
|
54
|
+
end
|
@@ -4,11 +4,19 @@ module EntityStore
|
|
4
4
|
|
5
5
|
ALL_METHOD = :all_events
|
6
6
|
|
7
|
+
def initialize(event_subscribers = nil)
|
8
|
+
@_event_subscribers = event_subscribers if event_subscribers
|
9
|
+
end
|
10
|
+
|
11
|
+
def event_subscribers
|
12
|
+
@_event_subscribers || EntityStore::Config.event_subscribers
|
13
|
+
end
|
14
|
+
|
7
15
|
def publish(entity_type, event)
|
8
|
-
publish_to_feed
|
16
|
+
publish_to_feed(entity_type, event)
|
9
17
|
|
10
|
-
subscribers_to(event.receiver_name).each
|
11
|
-
subscribers_to_all.each
|
18
|
+
subscribers_to(event.receiver_name).each { |s| send_to_subscriber(s, event.receiver_name, event) }
|
19
|
+
subscribers_to_all.each { |s| send_to_subscriber(s, ALL_METHOD, event) }
|
12
20
|
end
|
13
21
|
|
14
22
|
def send_to_subscriber subscriber, receiver_name, event
|
@@ -19,18 +27,48 @@ module EntityStore
|
|
19
27
|
end
|
20
28
|
|
21
29
|
def subscribers_to(event_name)
|
22
|
-
|
30
|
+
subscriber_lookup[event_name.to_sym].dup
|
23
31
|
end
|
24
32
|
|
25
33
|
def subscribers_to_all
|
26
|
-
|
34
|
+
subscribers_to(ALL_METHOD)
|
35
|
+
end
|
36
|
+
|
37
|
+
def subscriber_lookup_cache
|
38
|
+
@@lookup_cache ||= Hash.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def subscriber_lookup
|
42
|
+
return generate_subscriber_lookup unless EntityStore::Config.cache_event_subscribers
|
43
|
+
|
44
|
+
@lookup ||= begin
|
45
|
+
lookup_cache_key = event_subscribers.map(&:to_s).join
|
46
|
+
|
47
|
+
if subscriber_lookup_cache[lookup_cache_key]
|
48
|
+
subscriber_lookup_cache[lookup_cache_key]
|
49
|
+
else
|
50
|
+
subscriber_lookup_cache[lookup_cache_key] = generate_subscriber_lookup
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_subscriber_lookup
|
56
|
+
lookup = Hash.new { |h, k| h[k] = Array.new }
|
57
|
+
|
58
|
+
subscribers.each do |s|
|
59
|
+
s.instance_methods.each do |m|
|
60
|
+
lookup[m] << s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
lookup
|
27
65
|
end
|
28
66
|
|
29
67
|
def subscribers
|
30
|
-
|
68
|
+
event_subscribers.map do |subscriber|
|
31
69
|
case subscriber
|
32
70
|
when String
|
33
|
-
|
71
|
+
EntityStore::Config.load_type(subscriber)
|
34
72
|
else
|
35
73
|
subscriber
|
36
74
|
end
|
@@ -46,17 +84,17 @@ module EntityStore
|
|
46
84
|
end
|
47
85
|
|
48
86
|
# Public - replay events of a given type to a given subscriber
|
49
|
-
#
|
87
|
+
#
|
50
88
|
# since - Time reference point
|
51
89
|
# type - String type name of event
|
52
|
-
# subscriber - Class of the subscriber to replay events to
|
53
|
-
#
|
90
|
+
# subscriber - Class of the subscriber to replay events to
|
91
|
+
#
|
54
92
|
# Returns nothing
|
55
93
|
def replay(since, type, subscriber)
|
56
94
|
max_items = 100
|
57
95
|
event_data_objects = feed_store.get_events(since, type, max_items)
|
58
96
|
|
59
|
-
while event_data_objects.count > 0 do
|
97
|
+
while event_data_objects.count > 0 do
|
60
98
|
event_data_objects.each do |event_data_object|
|
61
99
|
begin
|
62
100
|
event = EntityStore::Config.load_type(event_data_object.type).new(event_data_object.attrs)
|
data/lib/entity_store/store.rb
CHANGED
@@ -2,10 +2,19 @@ module EntityStore
|
|
2
2
|
class Store
|
3
3
|
include Logging
|
4
4
|
|
5
|
+
def initialize(storage_client = nil, event_bus = nil)
|
6
|
+
@_storage_client = storage_client if storage_client
|
7
|
+
@_event_bus = event_bus if event_bus
|
8
|
+
end
|
9
|
+
|
5
10
|
def storage_client
|
6
11
|
@_storage_client ||= EntityStore::Config.store
|
7
12
|
end
|
8
13
|
|
14
|
+
def event_bus
|
15
|
+
@_event_bus ||= EventBus.new
|
16
|
+
end
|
17
|
+
|
9
18
|
def add(entity)
|
10
19
|
entity.id = storage_client.add_entity(entity)
|
11
20
|
add_events(entity)
|
@@ -31,8 +40,10 @@ module EntityStore
|
|
31
40
|
entity.id = storage_client.add_entity(entity)
|
32
41
|
end
|
33
42
|
|
34
|
-
add_events(entity)
|
35
|
-
|
43
|
+
added_events = add_events(entity)
|
44
|
+
|
45
|
+
if entity.snapshot_due? || added_events >= Config.snapshot_threshold
|
46
|
+
snapshot_entity(entity)
|
36
47
|
end
|
37
48
|
|
38
49
|
# publish version increment signal event to the bus
|
@@ -44,6 +55,33 @@ module EntityStore
|
|
44
55
|
raise e
|
45
56
|
end
|
46
57
|
|
58
|
+
# Upsert an entity where events have existed previously
|
59
|
+
# for example when migrating data
|
60
|
+
#
|
61
|
+
# Please note this method requires that the events expose their id property
|
62
|
+
# as a method named _id.
|
63
|
+
#
|
64
|
+
def upsert(entity)
|
65
|
+
unless entity.pending_events.empty?
|
66
|
+
entity.version = entity.pending_events.map(&:entity_version).max || 1
|
67
|
+
|
68
|
+
if entity.id
|
69
|
+
storage_client.save_entity(entity)
|
70
|
+
else
|
71
|
+
entity.id = storage_client.add_entity(entity)
|
72
|
+
end
|
73
|
+
|
74
|
+
upsert_events(entity)
|
75
|
+
|
76
|
+
# publish version increment signal event to the bus
|
77
|
+
event_bus.publish(entity.type, entity.generate_version_incremented_event)
|
78
|
+
end
|
79
|
+
entity
|
80
|
+
rescue => e
|
81
|
+
log_error "Store#upsert error: #{e.inspect} - #{entity.inspect}", e
|
82
|
+
raise e
|
83
|
+
end
|
84
|
+
|
47
85
|
def snapshot_entity(entity)
|
48
86
|
log_info { "Store#snapshot_entity : Snapshotting #{entity.id}"}
|
49
87
|
storage_client.snapshot_entity(entity)
|
@@ -69,9 +107,22 @@ module EntityStore
|
|
69
107
|
end
|
70
108
|
storage_client.add_events(items)
|
71
109
|
|
72
|
-
|
110
|
+
items.each { |e| event_bus.publish(entity.type, e) }
|
111
|
+
|
112
|
+
entity.clear_pending_events
|
113
|
+
items.count
|
114
|
+
end
|
115
|
+
|
116
|
+
def upsert_events(entity)
|
117
|
+
items = entity.pending_events.map do |event|
|
118
|
+
event.entity_id ||= entity.id.to_s
|
119
|
+
event.entity_version ||= entity.version
|
120
|
+
event
|
121
|
+
end
|
122
|
+
|
123
|
+
filtered_items = storage_client.upsert_events(items)
|
73
124
|
|
74
|
-
|
125
|
+
filtered_items.each { |e| event_bus.publish(entity.type, e) }
|
75
126
|
|
76
127
|
entity.clear_pending_events
|
77
128
|
end
|
@@ -146,10 +197,6 @@ module EntityStore
|
|
146
197
|
@_storage_client = nil
|
147
198
|
end
|
148
199
|
|
149
|
-
def event_bus
|
150
|
-
@_event_bus ||= EventBus.new
|
151
|
-
end
|
152
|
-
|
153
200
|
# Public: returns an array representing a full audit trail for the entity.
|
154
201
|
# After each event is applied the state of the entity is rendered.
|
155
202
|
# Optionally accepts a block which should return true or false to indicate
|
data/lib/entity_store/version.rb
CHANGED
data/lib/entity_store.rb
CHANGED
@@ -84,7 +84,7 @@ describe Entity do
|
|
84
84
|
subject { DummyEntity.new(1) }
|
85
85
|
|
86
86
|
it "should raise a readable error" do
|
87
|
-
expect { subject }.to raise_error(RuntimeError,
|
87
|
+
expect { subject }.to raise_error(RuntimeError, /\ADo not know how to create DummyEntity from (Integer|Fixnum)\z/)
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -74,6 +74,39 @@ describe Store do
|
|
74
74
|
|
75
75
|
end
|
76
76
|
|
77
|
+
describe "#upsert_events" do
|
78
|
+
before(:each) do
|
79
|
+
@entity = DummyEntityForStore.new(:name => random_string)
|
80
|
+
@entity.id = random_string
|
81
|
+
@entity.version = random_integer
|
82
|
+
@entity.pending_events << double(Event, :entity_id => @entity.id, :entity_version => @entity.version)
|
83
|
+
@entity.pending_events << double(Event, :entity_id => @entity.id, :entity_version => @entity.version)
|
84
|
+
@entity.pending_events << double(Event, :entity_id => @entity.id, :entity_version => @entity.version)
|
85
|
+
@storage_client = double("StorageClient", :upsert_events => filtered_events)
|
86
|
+
@store = Store.new
|
87
|
+
@store.stub(:storage_client) { @storage_client }
|
88
|
+
@event_bus = double(EventBus, :publish => true)
|
89
|
+
@store.stub(:event_bus) { @event_bus}
|
90
|
+
end
|
91
|
+
|
92
|
+
subject { @store.upsert_events(@entity) }
|
93
|
+
|
94
|
+
let(:filtered_events) { @entity.pending_events.take(2) }
|
95
|
+
|
96
|
+
it "adds each of the events" do
|
97
|
+
@storage_client.should_receive(:upsert_events).with(@entity.pending_events)
|
98
|
+
subject
|
99
|
+
end
|
100
|
+
|
101
|
+
it "publishes each event to the EventBus" do
|
102
|
+
filtered_events.each do |e|
|
103
|
+
@event_bus.should_receive(:publish).with(@entity.type, e)
|
104
|
+
end
|
105
|
+
subject
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
77
110
|
describe "#save" do
|
78
111
|
|
79
112
|
before(:each) do
|
@@ -81,7 +114,7 @@ describe Store do
|
|
81
114
|
@entity = DummyEntityForStore.new(:id => random_string)
|
82
115
|
@storage_client = double("StorageClient", :save_entity => true)
|
83
116
|
@store = Store.new
|
84
|
-
@store.stub(:add_events).
|
117
|
+
@store.stub(:add_events) { |entity| entity.pending_events.count }
|
85
118
|
@store.stub(:storage_client) { @storage_client }
|
86
119
|
@entity.stub(:pending_events) { [ double('Event') ] }
|
87
120
|
end
|
@@ -161,8 +194,23 @@ describe Store do
|
|
161
194
|
subject
|
162
195
|
end
|
163
196
|
end
|
164
|
-
end
|
165
197
|
|
198
|
+
context "when flushed events exceeds snapshotting threshold" do
|
199
|
+
before(:each) do
|
200
|
+
@entity.version = 1
|
201
|
+
@entity.stub(:pending_events) do
|
202
|
+
(1..EntityStore::Config.snapshot_threshold).map do
|
203
|
+
double('Event')
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should snapshot the entity" do
|
209
|
+
@storage_client.should_receive(:snapshot_entity).with(@entity)
|
210
|
+
subject
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
166
214
|
end
|
167
215
|
|
168
216
|
describe "getters" do
|
data/spec/entity_store_spec.rb
CHANGED
@@ -26,7 +26,17 @@ class DummyEntitySubscriber
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def dummy_entity_name_set(event)
|
29
|
-
|
29
|
+
self.class.event_name = event.name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class AnotherEntitySubscriber
|
34
|
+
class << self
|
35
|
+
attr_accessor :event_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def dummy_entity_name_set(event)
|
39
|
+
self.class.event_name = event.name
|
30
40
|
end
|
31
41
|
end
|
32
42
|
|
@@ -80,6 +90,49 @@ class DummyStore
|
|
80
90
|
end
|
81
91
|
end
|
82
92
|
|
93
|
+
describe "creation without static instances" do
|
94
|
+
let(:store) do
|
95
|
+
storage_client = DummyStore.new
|
96
|
+
Store.new(storage_client, event_bus)
|
97
|
+
end
|
98
|
+
|
99
|
+
let(:event_bus) do
|
100
|
+
event_subscribers = []
|
101
|
+
event_subscribers << AnotherEntitySubscriber
|
102
|
+
|
103
|
+
EventBus.new(event_subscribers)
|
104
|
+
end
|
105
|
+
|
106
|
+
before do
|
107
|
+
EntityStore::Config.setup do |config|
|
108
|
+
config.store = DummyStore.new
|
109
|
+
config.event_subscribers << DummyEntitySubscriber
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "when save entity" do
|
114
|
+
let(:name) { random_string }
|
115
|
+
before(:each) do
|
116
|
+
@entity = DummyEntity.new
|
117
|
+
@entity.set_name name
|
118
|
+
@id = store.save @entity
|
119
|
+
end
|
120
|
+
|
121
|
+
it "does not publish event to the non configured subscriber" do
|
122
|
+
DummyEntitySubscriber.event_name.should_not eq(name)
|
123
|
+
end
|
124
|
+
it "publishes event to the subscriber" do
|
125
|
+
AnotherEntitySubscriber.event_name.should eq(name)
|
126
|
+
end
|
127
|
+
it "is retrievable with the events applied" do
|
128
|
+
store.get(@entity.id).name.should eq(name)
|
129
|
+
end
|
130
|
+
it "is not retrievable from the static store instance" do
|
131
|
+
EntityStore::Store.new.get(@entity.id).should eq(nil)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
83
136
|
describe "end to end" do
|
84
137
|
before(:each) do
|
85
138
|
EntityStore::Config.setup do |config|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: entity_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Bird
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bson
|
@@ -57,7 +57,6 @@ files:
|
|
57
57
|
- lib/entity_store/not_found.rb
|
58
58
|
- lib/entity_store/store.rb
|
59
59
|
- lib/entity_store/time_factory.rb
|
60
|
-
- lib/entity_store/utils.rb
|
61
60
|
- lib/entity_store/version.rb
|
62
61
|
- lib/tasks/entity_store.rake
|
63
62
|
- spec/entity_store/config_spec.rb
|
@@ -87,8 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
86
|
- !ruby/object:Gem::Version
|
88
87
|
version: '0'
|
89
88
|
requirements: []
|
90
|
-
|
91
|
-
rubygems_version: 2.7.4
|
89
|
+
rubygems_version: 3.2.33
|
92
90
|
signing_key:
|
93
91
|
specification_version: 4
|
94
92
|
summary: Event sourced entity store with a replaceable body
|