File Coverage

blib/lib/EventStore/Tiny.pm
Criterion Covered Total %
statement 72 72 100.0
branch 12 12 100.0
condition 13 14 92.8
subroutine 19 19 100.0
pod 8 8 100.0
total 124 125 99.2


line stmt bran cond sub pod time code
1             package EventStore::Tiny;
2              
3 6     6   133847 use strict;
  6         30  
  6         147  
4 6     6   26 use warnings;
  6         12  
  6         154  
5              
6 6     6   2040 use EventStore::Tiny::Logger;
  6         16  
  6         194  
7 6     6   2350 use EventStore::Tiny::Event;
  6         19  
  6         173  
8 6     6   2402 use EventStore::Tiny::DataEvent;
  6         11  
  6         157  
9 6     6   1972 use EventStore::Tiny::EventStream;
  6         12  
  6         126  
10 6     6   1763 use EventStore::Tiny::Snapshot;
  6         12  
  6         133  
11              
12 6     6   1892 use Clone qw(clone);
  6         10922  
  6         278  
13 6     6   2754 use Storable;
  6         14500  
  6         278  
14 6     6   2305 use Data::Compare; # Exports Compare()
  6         47467  
  6         36  
15              
16             # Enable handling of CODE refs (as event actions are code refs)
17             $Storable::Deparse = 1;
18             $Storable::Eval = 1;
19              
20             our $VERSION = '0.42';
21              
22             use Class::Tiny {
23 7         140 registry => sub {{}},
24 6         654 events => sub {EventStore::Tiny::EventStream->new(
25             logger => shift->logger)},
26 5         78 init_data => sub {{}},
27 4         38 logger => sub {EventStore::Tiny::Logger->log_cb},
28 6         83 cache_distance => 0, # Default: store snapshot each time. no caching: undef
29 6     6   17026 }, '_cached_snapshot';
  6         11  
30              
31             # Class method to construct
32             sub new_from_file {
33 1     1 1 3727 my (undef, $fn) = @_;
34 1         4 return retrieve($fn);
35             }
36              
37             sub store_to_file {
38 1     1 1 1912 my ($self, $fn) = @_;
39 1         21 return store($self, $fn);
40             }
41              
42             sub register_event {
43 15     15 1 7281 my ($self, $name, $transformation) = @_;
44              
45 15         223 return $self->registry->{$name} = EventStore::Tiny::Event->new(
46             name => $name,
47             transformation => $transformation,
48             logger => $self->logger,
49             );
50             }
51              
52             sub event_names {
53 2     2 1 1298 my $self = shift;
54 2         2 return [sort keys %{$self->registry}];
  2         30  
55             }
56              
57             sub store_event {
58 247     247 1 8179 my ($self, $name, $data) = @_;
59              
60             # Lookup template event
61 247         2778 my $template = $self->registry->{$name};
62 247 100       1035 die "Unknown event: $name!\n" unless defined $template;
63              
64             # Specialize event with new data
65 246         450 my $event = EventStore::Tiny::DataEvent->new_from_template(
66             $template, $data
67             );
68              
69             # Done
70 246         3379 return $self->events->add_event($event);
71             }
72              
73             sub init_state {
74 11     11 1 60 my $self = shift;
75              
76             # Clone init data
77 11         155 return clone($self->init_data);
78             }
79              
80             sub snapshot {
81 34     34 1 45253 my ($self, $timestamp) = @_;
82              
83             # Work on latest timestamp if not specified
84 34   100     457 $timestamp //= $self->events->last_timestamp;
85 34         836 my $es = $self->events->before($timestamp);
86              
87             # Check if the cached snapshot can be used
88 34         873 my $state;
89 34         403 my $cached_sn = $self->_cached_snapshot;
90 34 100 100     432 if (defined $cached_sn and $cached_sn->timestamp <= $timestamp) {
91              
92             # Calculate what still needs to be applied
93 23         355 $es = $es->after($cached_sn->timestamp);
94              
95             # Nothing? Great!
96 23 100       627 return $self->_cached_snapshot if $es->size == 0;
97              
98             # Still something? Start here
99 13         209 $state = clone $cached_sn->state;
100             }
101              
102             # Calculate snapshot
103 24   66     267 $state //= $self->init_state;
104 24   100     374 my $snapshot = EventStore::Tiny::Snapshot->new(
105             state => $es->apply_to($state, $self->logger),
106             timestamp => $es->last_timestamp // 0,
107             );
108              
109             # Caching disabled: done
110 24 100       375 return $snapshot unless defined $self->cache_distance;
111              
112             # Cache snapshot if no cache present yet, but neccessary
113 22 100 100     373 $self->_cached_snapshot($snapshot)
114             if not defined $self->_cached_snapshot and $es->size > 0;
115              
116             # Cache snapshot if new event count > cache size
117             $self->_cached_snapshot($snapshot)
118 22 100       172 if @{$es->events} > $self->cache_distance;
  22         304  
119              
120             # Done
121 22         654 return $snapshot;
122             }
123              
124             sub is_correct_snapshot {
125 4     4 1 38 my ($self, $snapshot) = @_;
126              
127             # Replay events before snapshot time
128 4         47 my $our_sn = $self->snapshot($snapshot->timestamp);
129              
130             # True iff the generated state looks the same
131 4         102 return Compare($snapshot->state, $our_sn->state);
132             }
133              
134             1;
135              
136             =pod
137              
138             =encoding utf-8
139              
140             =head1 NAME
141              
142             EventStore::Tiny - A minimal event sourcing framework.
143              
144             =begin html
145              
146            

147              
148            
149             CPAN version
150            
151             Travis CI tests
152            
153             Codecov test coverage
154            
155             Coveralls test coverage
156            
157             CPANTS kwalitee score
158              
159            

160              
161            
162             CPAN testers reports
163            
164             CPAN testers matrix
165            
166             GitHub repository
167            
168             GitHub issue tracker
169              
170            

171              
172             =end html
173              
174             =head1 SYNOPSIS
175              
176             use EventStore::Tiny;
177              
178             my $store = EventStore::Tiny->new;
179              
180             # Register event type
181             $store->register_event(UserAdded => sub {
182             my ($state, $data) = @_;
183              
184             # Use $data to inject the new user into the given $state
185             $state->{users}{$data->{id}} = {
186             name => $data->{name},
187             };
188             });
189              
190             # ...
191              
192             # Store an event instance represented by type and data
193             $store->store_event(UserAdded => {id => 17, name => 'Bob'});
194              
195             # ...
196              
197             # Work with the current state snapshot generated by event application
198             say 'His name is ' . $store->snapshot->state->{users}{17}{name}; # Bob
199              
200             =head1 DESCRIPTION
201              
202             In Event Sourcing, the state of a system is calculated as the application of a stream of events representing each change of the system. This framework is a minimal approach to use these mechanics in simple perl systems and offers these features:
203              
204             =over 2
205              
206             =item *
207              
208             Flexible snapshots (high-resolution timestamps) and event substreams.
209              
210             =item *
211              
212             Customizable event logging.
213              
214             =item *
215              
216             Simple storage solution for events in the file system.
217              
218             =item *
219              
220             Transparent snapshot caching mechanism to improve performance.
221              
222             =back
223              
224             The internal state of the system needs to be represented by a simple (nested) hash and all events need to operate on this hash only (by side-effect).
225              
226             =head1 REFERENCE
227              
228             EventStore::Tiny implements the following attributes and methods, grouped by topic.
229              
230             =head2 CONSTRUCTION AND PERSISTENCE
231              
232             =head3 new
233              
234             my $store = EventStore::Tiny->new(init_data => {answer = 42});
235              
236             Standard constructor. Understands all attributes as arguments. For most use cases, these are the sensible arguments:
237              
238             =over 4
239              
240             =item init_data
241              
242             A hashref representing the initial state. B>
243              
244             =item cache_distance
245              
246             The number of events after a new snapshot is cached for accellerated access. 0 means the cache is updated after each event. undef means the system does not use any caching. B
247              
248             =item logger
249              
250             A subref (callback) which will be called each time an event is applied to the state. The callback gets this event as its only argument. B>
251              
252             =back
253              
254             =head3 new_from_file
255              
256             my $store = EventStore::Tiny->new_from_file($filename);
257              
258             Deserializes an existing store object which was Ld before.
259              
260             =head3 store_to_file
261              
262             $store->store_to_file($filename);
263              
264             Serializes the store object to the file system. It can be deserialized via L later.
265              
266             =head2 EVENT SOURCING WORKFLOW
267              
268             =head3 register_event
269              
270             $store->register_event(ConnectionRemoved => sub {
271             my ($state, $data) = @_;
272             # Change $state depending on $data (by side-effect)
273             });
274              
275             Stores an event type in the system by name and action on the C<$state>. Events of this type can be added later to the event store by setting concrete C<$data> with L.
276              
277             =head3 store_event
278              
279             $store->store_event(ConnectionRemoved => {id => 42});
280              
281             Stores a concrete instance of an event type in the event store. The instance is defined by its event type name and a hash of data used by the subref the event uses to manipulate the state.
282              
283             =head3 snapshot
284              
285             my $state1 = $store->snapshot->state;
286              
287             my $snapshot = $store->snapshot(1234217421);
288             my $state2 = $snapshot->state;
289             my $timestamp = $snapshot->timestamp; # 1234217421
290              
291             Returns a L object which basically consists of the corresponding state of the system (represented by a hashref) and the timestamp of the last used event. Snapshots are selected by the given argument timestamp, which returns the current snapshot at the given time. If no timestamp is given, the snapshot represents the last state of the system.
292              
293             =head2 INTROSPECTION
294              
295             =head3 event_names
296              
297             my $types = $store->event_names;
298              
299             Returns an arrayref containing all event type names of registered events, sorted by name. These names are the values of L.
300              
301             =head3 registry
302              
303             my $user_added = $store->registry->{UserAdded};
304              
305             Returns a hashref with event type names as keys and event types as values, which are L instances. Should be manipulated by L only.
306              
307             =head3 events
308              
309             my $event_stream = $store->events;
310              
311             Returns the internal L object that stores all concrete events (L instances). Should be manipulated by L only. Events should never be changed or removed.
312              
313             =head3 init_state
314              
315             my $state = $store->init_state;
316              
317             Returns a cloned copy of the ininitial state all events are applied on, which was defined by L as a hashref.
318              
319             =head2 OTHER
320              
321             =head3 is_correct_snapshot
322              
323             if ($store->is_correct_snapshot($snapshot)) {
324             # ...
325             }
326              
327             Checks if a given L instance is a valid snapshot of our L event store. Mostly used for testing.
328              
329             =head1 REPOSITORY AND ISSUE TRACKER
330              
331             EventStore::Tiny's source repository is hosted on L together with an issue tracker.
332              
333             =head1 COPYRIGHT AND LICENSE
334              
335             Copyright (c) 2018 L (L<@memowe|https://github.com/memowe>, L)
336              
337             Released under the MIT License (see LICENSE.txt for details).
338              
339             =head2 CONTRIBUTORS
340              
341             =over 2
342              
343             =item *
344              
345             Mohammad S Anwar (L<@manwar|https://github.com/manwar>)
346              
347             =item *
348              
349             Toby Inkster (L<@tobyink|https://github.com/tobyink>)
350              
351             =back
352              
353             =cut