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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02a69a4134e4ef998a3258424ed1ff35e171babf83d72665ab37464aaba1abbe
4
- data.tar.gz: 2b9af465f21d0131c71b80cd3a9e5fb08579ebfbe27e9c2595e45b9ccffba622
3
+ metadata.gz: d4575519de291e09a6f02085a07daeb09818edc01a5d8895204d443fc03db6b9
4
+ data.tar.gz: ce27f46beb087f7bcd48b6d5e690db1239e75adaffdcb524b1c24df0cbeea49f
5
5
  SHA512:
6
- metadata.gz: 893058a17b8cef182d8a7349ad03107b00361d66af25837f44846aa8daed83cf9bbc38cef57874a753e3d79e7bf47e8c66a4612a25564a893f5188fde7e7119a
7
- data.tar.gz: 8b10d320996be80cfc718fa709339132261104b0edb7b6fe3b286757dd5ec9f3535400d1214a6d7af4ef21abc4fb612e47ef0bbdfde9c99a290c34bee226cc48
6
+ metadata.gz: de737107e012f7a07b5aea26280b425a6db374e13325054344e7c9fac196ee98bde2dbcc7d231fc0cd28b26992d805188a06d727373ef7c46e3552598c9c6e52
7
+ data.tar.gz: a624bb070c990b68b0a436d07c940f656238a244a80bfe2c789f3bb7feb4f8de0e66c65696496d75b936557e5bb4465466cd1e5f05f1b4583e8cf3bab3cc31ee
@@ -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 entity_type, event
16
+ publish_to_feed(entity_type, event)
9
17
 
10
- subscribers_to(event.receiver_name).each do |s| send_to_subscriber s, event.receiver_name, event end
11
- subscribers_to_all.each do |s| send_to_subscriber s, ALL_METHOD, event end
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
- subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
30
+ subscriber_lookup[event_name.to_sym].dup
23
31
  end
24
32
 
25
33
  def subscribers_to_all
26
- subscribers.select { |s| s.instance_methods.include?(ALL_METHOD) }
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
- EntityStore::Config.event_subscribers.map do |subscriber|
68
+ event_subscribers.map do |subscriber|
31
69
  case subscriber
32
70
  when String
33
- Utils.get_type_constant(subscriber)
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)
@@ -1,7 +1,7 @@
1
1
  module EntityStore
2
2
  class NotFound < StandardError
3
- def initialise(id)
3
+ def initialize(id)
4
4
  super("no item with id #{id} could be found")
5
5
  end
6
6
  end
7
- end
7
+ end
@@ -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) do
35
- snapshot_entity(entity) if entity.snapshot_due?
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
- yield if block_given?
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
- items.each {|e| event_bus.publish(entity.type, e) }
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
@@ -1,3 +1,3 @@
1
1
  module EntityStore
2
- VERSION = "1.2.0".freeze
2
+ VERSION = "1.5.0".freeze
3
3
  end
data/lib/entity_store.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  module EntityStore
2
-
3
- require_relative 'entity_store/utils'
4
2
  require_relative 'entity_store/logging'
5
3
  require_relative 'entity_store/config'
6
4
  require_relative 'entity_store/time_factory'
@@ -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, "Do not know how to create DummyEntity from Fixnum")
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).and_yield
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
@@ -26,7 +26,17 @@ class DummyEntitySubscriber
26
26
  end
27
27
 
28
28
  def dummy_entity_name_set(event)
29
- DummyEntitySubscriber.event_name = event.name
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.2.0
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: 2018-03-12 00:00:00.000000000 Z
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
- rubyforge_project:
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
@@ -1,8 +0,0 @@
1
- module EntityStore
2
- module Utils
3
- def self.get_type_constant(type_name)
4
- type_name.split('::').inject(Object) { |obj, name| obj.const_get(name) }
5
- end
6
- end
7
- end
8
-