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   127846 use strict;
  6         30  
  6         140  
4 6     6   23 use warnings;
  6         10  
  6         150  
5              
6 6     6   1915 use EventStore::Tiny::Logger;
  6         18  
  6         192  
7 6     6   2378 use EventStore::Tiny::Event;
  6         18  
  6         189  
8 6     6   2220 use EventStore::Tiny::DataEvent;
  6         13  
  6         142  
9 6     6   1896 use EventStore::Tiny::EventStream;
  6         13  
  6         141  
10 6     6   1919 use EventStore::Tiny::Snapshot;
  6         11  
  6         145  
11              
12 6     6   2002 use Clone qw(clone);
  6         11455  
  6         337  
13 6     6   2813 use Storable;
  6         14301  
  6         262  
14 6     6   2185 use Data::Compare; # Exports Compare()
  6         48973  
  6         35  
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.43';
21              
22             use Class::Tiny {
23 7         140 registry => sub {{}},
24 6         652 events => sub {EventStore::Tiny::EventStream->new(
25             logger => shift->logger)},
26 5         83 init_data => sub {{}},
27 4         37 logger => sub {EventStore::Tiny::Logger->log_cb},
28 6         81 cache_distance => 0, # Default: store snapshot each time. no caching: undef
29 6     6   17560 }, '_cached_snapshot';
  6         15  
30              
31             # Class method to construct
32             sub new_from_file {
33 1     1 1 3968 my (undef, $fn) = @_;
34 1         6 return retrieve($fn);
35             }
36              
37             sub store_to_file {
38 1     1 1 1988 my ($self, $fn) = @_;
39 1         31 return store($self, $fn);
40             }
41              
42             sub register_event {
43 15     15 1 7217 my ($self, $name, $transformation) = @_;
44              
45 15         219 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 1319 my $self = shift;
54 2         2 return [sort keys %{$self->registry}];
  2         31  
55             }
56              
57             sub store_event {
58 247     247 1 9125 my ($self, $name, $data) = @_;
59              
60             # Lookup template event
61 247         2709 my $template = $self->registry->{$name};
62 247 100       964 die "Unknown event: $name!\n" unless defined $template;
63              
64             # Specialize event with new data
65 246         455 my $event = EventStore::Tiny::DataEvent->new_from_template(
66             $template, $data
67             );
68              
69             # Done
70 246         3247 return $self->events->add_event($event);
71             }
72              
73             sub init_state {
74 11     11 1 71 my $self = shift;
75              
76             # Clone init data
77 11         157 return clone($self->init_data);
78             }
79              
80             sub snapshot {
81 34     34 1 49439 my ($self, $timestamp) = @_;
82              
83             # Work on latest timestamp if not specified
84 34   100     505 $timestamp //= $self->events->last_timestamp;
85 34         906 my $es = $self->events->before($timestamp);
86              
87             # Check if the cached snapshot can be used
88 34         661 my $state;
89 34         467 my $cached_sn = $self->_cached_snapshot;
90 34 100 100     544 if (defined $cached_sn and $cached_sn->timestamp <= $timestamp) {
91              
92             # Calculate what still needs to be applied
93 23         391 $es = $es->after($cached_sn->timestamp);
94              
95             # Nothing? Great!
96 23 100       656 return EventStore::Tiny::Snapshot->new(
97             state => clone($self->_cached_snapshot->state),
98             timestamp => $self->_cached_snapshot->timestamp,
99             ) if $es->size == 0;
100              
101             # Still something? Start here
102 13         202 $state = clone $cached_sn->state;
103             }
104              
105             # Calculate snapshot
106 24   66     253 $state //= $self->init_state;
107 24   100     540 my $snapshot = EventStore::Tiny::Snapshot->new(
108             state => $es->apply_to($state, $self->logger),
109             timestamp => $es->last_timestamp // 0,
110             );
111              
112             # Caching disabled: done
113 24 100       375 return $snapshot unless defined $self->cache_distance;
114              
115             # Cache snapshot if no cache present yet, but neccessary
116 22 100 100     343 $self->_cached_snapshot($snapshot)
117             if not defined $self->_cached_snapshot and $es->size > 0;
118              
119             # Cache snapshot if new event count > cache size
120             $self->_cached_snapshot($snapshot)
121 22 100       180 if @{$es->events} > $self->cache_distance;
  22         324  
122              
123             # Done
124 22         671 return $snapshot;
125             }
126              
127             sub is_correct_snapshot {
128 4     4 1 47 my ($self, $snapshot) = @_;
129              
130             # Replay events before snapshot time
131 4         51 my $our_sn = $self->snapshot($snapshot->timestamp);
132              
133             # True iff the generated state looks the same
134 4         100 return Compare($snapshot->state, $our_sn->state);
135             }
136              
137             1;
138              
139             =pod
140              
141             =encoding utf-8
142              
143             =head1 NAME
144              
145             EventStore::Tiny - A minimal event sourcing framework.
146              
147             =begin html
148              
149            

150              
151            
152             CPAN version
153            
154             Travis CI tests
155            
156             Codecov test coverage
157            
158             Coveralls test coverage
159            
160             CPANTS kwalitee score
161              
162            

163              
164            
165             CPAN testers reports
166            
167             CPAN testers matrix
168            
169             GitHub repository
170            
171             GitHub issue tracker
172              
173            

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