line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package EventStore::Tiny; |
2
|
|
|
|
|
|
|
|
3
|
6
|
|
|
6
|
|
143559
|
use strict; |
|
6
|
|
|
|
|
36
|
|
|
6
|
|
|
|
|
158
|
|
4
|
6
|
|
|
6
|
|
27
|
use warnings; |
|
6
|
|
|
|
|
9
|
|
|
6
|
|
|
|
|
130
|
|
5
|
|
|
|
|
|
|
|
6
|
6
|
|
|
6
|
|
2226
|
use EventStore::Tiny::Logger; |
|
6
|
|
|
|
|
16
|
|
|
6
|
|
|
|
|
177
|
|
7
|
6
|
|
|
6
|
|
2432
|
use EventStore::Tiny::Event; |
|
6
|
|
|
|
|
24
|
|
|
6
|
|
|
|
|
160
|
|
8
|
6
|
|
|
6
|
|
2456
|
use EventStore::Tiny::DataEvent; |
|
6
|
|
|
|
|
16
|
|
|
6
|
|
|
|
|
157
|
|
9
|
6
|
|
|
6
|
|
2307
|
use EventStore::Tiny::EventStream; |
|
6
|
|
|
|
|
19
|
|
|
6
|
|
|
|
|
147
|
|
10
|
6
|
|
|
6
|
|
2137
|
use EventStore::Tiny::Snapshot; |
|
6
|
|
|
|
|
16
|
|
|
6
|
|
|
|
|
173
|
|
11
|
|
|
|
|
|
|
|
12
|
6
|
|
|
6
|
|
2343
|
use Clone qw(clone); |
|
6
|
|
|
|
|
12692
|
|
|
6
|
|
|
|
|
278
|
|
13
|
6
|
|
|
6
|
|
3381
|
use Storable; |
|
6
|
|
|
|
|
16438
|
|
|
6
|
|
|
|
|
314
|
|
14
|
6
|
|
|
6
|
|
2839
|
use Data::Compare; # Exports Compare() |
|
6
|
|
|
|
|
57224
|
|
|
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.41'; |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
use Class::Tiny { |
23
|
7
|
|
|
|
|
201
|
registry => sub {{}}, |
24
|
6
|
|
|
|
|
728
|
events => sub {EventStore::Tiny::EventStream->new( |
25
|
|
|
|
|
|
|
logger => shift->logger)}, |
26
|
5
|
|
|
|
|
127
|
init_data => sub {{}}, |
27
|
4
|
|
|
|
|
82
|
logger => sub {EventStore::Tiny::Logger->log_cb}, |
28
|
6
|
|
|
|
|
84
|
cache_distance => 0, # Default: store snapshot each time. no caching: undef |
29
|
6
|
|
|
6
|
|
19593
|
}, '_cached_snapshot'; |
|
6
|
|
|
|
|
29
|
|
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# Class method to construct |
32
|
|
|
|
|
|
|
sub new_from_file { |
33
|
1
|
|
|
1
|
1
|
4939
|
my (undef, $fn) = @_; |
34
|
1
|
|
|
|
|
6
|
return retrieve($fn); |
35
|
|
|
|
|
|
|
} |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
sub store_to_file { |
38
|
1
|
|
|
1
|
1
|
2214
|
my ($self, $fn) = @_; |
39
|
1
|
|
|
|
|
30
|
return store($self, $fn); |
40
|
|
|
|
|
|
|
} |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
sub register_event { |
43
|
15
|
|
|
15
|
1
|
8047
|
my ($self, $name, $transformation) = @_; |
44
|
|
|
|
|
|
|
|
45
|
15
|
|
|
|
|
249
|
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
|
1528
|
my $self = shift; |
54
|
2
|
|
|
|
|
3
|
return [sort keys %{$self->registry}]; |
|
2
|
|
|
|
|
42
|
|
55
|
|
|
|
|
|
|
} |
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
sub store_event { |
58
|
247
|
|
|
247
|
1
|
9175
|
my ($self, $name, $data) = @_; |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
# Lookup template event |
61
|
247
|
|
|
|
|
3150
|
my $template = $self->registry->{$name}; |
62
|
247
|
100
|
|
|
|
1128
|
die "Unknown event: $name!\n" unless defined $template; |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
# Specialize event with new data |
65
|
246
|
|
|
|
|
511
|
my $event = EventStore::Tiny::DataEvent->new_from_template( |
66
|
|
|
|
|
|
|
$template, $data |
67
|
|
|
|
|
|
|
); |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
# Done |
70
|
246
|
|
|
|
|
4176
|
return $self->events->add_event($event); |
71
|
|
|
|
|
|
|
} |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
sub init_state { |
74
|
34
|
|
|
34
|
1
|
48
|
my $self = shift; |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
# Clone init data |
77
|
34
|
|
|
|
|
502
|
return clone($self->init_data); |
78
|
|
|
|
|
|
|
} |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
sub snapshot { |
81
|
34
|
|
|
34
|
1
|
52331
|
my ($self, $timestamp) = @_; |
82
|
34
|
|
|
|
|
79
|
my $state = $self->init_state; |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# Work on latest timestamp if not specified |
85
|
34
|
|
100
|
|
|
680
|
$timestamp //= $self->events->last_timestamp; |
86
|
34
|
|
|
|
|
962
|
my $es = $self->events->before($timestamp); |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
# Check if the cached snapshot can be used |
89
|
34
|
|
|
|
|
1318
|
my $cached_sn = $self->_cached_snapshot; |
90
|
34
|
100
|
100
|
|
|
522
|
if (defined $cached_sn and $cached_sn->timestamp <= $timestamp) { |
91
|
23
|
|
|
|
|
413
|
$state = clone $cached_sn->state; |
92
|
23
|
|
|
|
|
632
|
$es = $es->after($cached_sn->timestamp); |
93
|
|
|
|
|
|
|
} |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
# Calculate snapshot |
96
|
34
|
|
100
|
|
|
1205
|
my $snapshot = EventStore::Tiny::Snapshot->new( |
97
|
|
|
|
|
|
|
state => $es->apply_to($state, $self->logger), |
98
|
|
|
|
|
|
|
timestamp => $es->last_timestamp // 0, |
99
|
|
|
|
|
|
|
); |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
# Caching disabled: done |
102
|
34
|
100
|
|
|
|
592
|
return $snapshot unless defined $self->cache_distance; |
103
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
# Cache snapshot if no cache present yet, but neccessary |
105
|
32
|
100
|
100
|
|
|
570
|
$self->_cached_snapshot($snapshot) |
106
|
|
|
|
|
|
|
if not defined $self->_cached_snapshot and $es->size > 0; |
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
# Cache snapshot if new event count > cache size |
109
|
|
|
|
|
|
|
$self->_cached_snapshot($snapshot) |
110
|
32
|
100
|
|
|
|
235
|
if @{$es->events} > $self->cache_distance; |
|
32
|
|
|
|
|
472
|
|
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
# Done |
113
|
32
|
|
|
|
|
896
|
return $snapshot; |
114
|
|
|
|
|
|
|
} |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
sub is_correct_snapshot { |
117
|
4
|
|
|
4
|
1
|
62
|
my ($self, $snapshot) = @_; |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
# Replay events before snapshot time |
120
|
4
|
|
|
|
|
63
|
my $our_sn = $self->snapshot($snapshot->timestamp); |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
# True iff the generated state looks the same |
123
|
4
|
|
|
|
|
116
|
return Compare($snapshot->state, $our_sn->state); |
124
|
|
|
|
|
|
|
} |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
1; |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
=pod |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
=encoding utf-8 |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
=head1 NAME |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
EventStore::Tiny - A minimal event sourcing framework. |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
=begin html |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
=end html |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
=head1 SYNOPSIS |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
use EventStore::Tiny; |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
my $store = EventStore::Tiny->new; |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
# Register event type |
173
|
|
|
|
|
|
|
$store->register_event(UserAdded => sub { |
174
|
|
|
|
|
|
|
my ($state, $data) = @_; |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
# Use $data to inject the new user into the given $state |
177
|
|
|
|
|
|
|
$state->{users}{$data->{id}} = { |
178
|
|
|
|
|
|
|
name => $data->{name}, |
179
|
|
|
|
|
|
|
}; |
180
|
|
|
|
|
|
|
}); |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
# ... |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
# Store an event instance represented by type and data |
185
|
|
|
|
|
|
|
$store->store_event(UserAdded => {id => 17, name => 'Bob'}); |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
# ... |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
# Work with the current state snapshot generated by event application |
190
|
|
|
|
|
|
|
say 'His name is ' . $store->snapshot->state->{users}{17}{name}; # Bob |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
=head1 DESCRIPTION |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
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: |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
=over 2 |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
=item * |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
Flexible snapshots (high-resolution timestamps) and event substreams. |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
=item * |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
Customizable event logging. |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
=item * |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
Simple storage solution for events in the file system. |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
=item * |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
Transparent snapshot caching mechanism to improve performance. |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=back |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
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). |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
=head1 REFERENCE |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
EventStore::Tiny implements the following attributes and methods, grouped by topic. |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
=head2 CONSTRUCTION AND PERSISTENCE |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
=head3 new |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
my $store = EventStore::Tiny->new(init_data => {answer = 42}); |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
Standard constructor. Understands all attributes as arguments. For most use cases, these are the sensible arguments: |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
=over 4 |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
=item init_data |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
A hashref representing the initial state. B> |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
=item cache_distance |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
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 |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
=item logger |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
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> |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
=back |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
=head3 new_from_file |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
my $store = EventStore::Tiny->new_from_file($filename); |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
Deserializes an existing store object which was Ld before. |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
=head3 store_to_file |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
$store->store_to_file($filename); |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
Serializes the store object to the file system. It can be deserialized via L later. |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
=head2 EVENT SOURCING WORKFLOW |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
=head3 register_event |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
$store->register_event(ConnectionRemoved => sub { |
263
|
|
|
|
|
|
|
my ($state, $data) = @_; |
264
|
|
|
|
|
|
|
# Change $state depending on $data (by side-effect) |
265
|
|
|
|
|
|
|
}); |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
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. |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
=head3 store_event |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
$store->store_event(ConnectionRemoved => {id => 42}); |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
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. |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
=head3 snapshot |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
my $state1 = $store->snapshot->state; |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
my $snapshot = $store->snapshot(1234217421); |
280
|
|
|
|
|
|
|
my $state2 = $snapshot->state; |
281
|
|
|
|
|
|
|
my $timestamp = $snapshot->timestamp; # 1234217421 |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
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. |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
=head2 INTROSPECTION |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
=head3 event_names |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
my $types = $store->event_names; |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
Returns an arrayref containing all event type names of registered events, sorted by name. These names are the values of L. |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=head3 registry |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
my $user_added = $store->registry->{UserAdded}; |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
Returns a hashref with event type names as keys and event types as values, which are L instances. Should be manipulated by L only. |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
=head3 events |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
my $event_stream = $store->events; |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
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. |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
=head3 init_state |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
my $state = $store->init_state; |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
Returns a cloned copy of the ininitial state all events are applied on, which was defined by L as a hashref. |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
=head2 OTHER |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
=head3 is_correct_snapshot |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
if ($store->is_correct_snapshot($snapshot)) { |
316
|
|
|
|
|
|
|
# ... |
317
|
|
|
|
|
|
|
} |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
Checks if a given L instance is a valid snapshot of our L event store. Mostly used for testing. |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
=head1 REPOSITORY AND ISSUE TRACKER |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
EventStore::Tiny's source repository is hosted on L together with an issue tracker. |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
Copyright (c) 2018 L (L<@memowe|https://github.com/memowe>, L) |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
Released under the MIT License (see LICENSE.txt for details). |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
=head2 CONTRIBUTORS |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
=over 2 |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
=item * |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
Mohammad S Anwar (L<@manwar|https://github.com/manwar>) |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
=item * |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
Toby Inkster (L<@tobyink|https://github.com/tobyink>) |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
=back |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
=cut |