eventsimple 1.0.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 +7 -0
- data/.DS_Store +0 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +164 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +320 -0
- data/Guardfile +35 -0
- data/LICENSE +22 -0
- data/README.md +510 -0
- data/Rakefile +17 -0
- data/app/controllers/eventsimple/application_controller.rb +15 -0
- data/app/controllers/eventsimple/entities_controller.rb +59 -0
- data/app/controllers/eventsimple/home_controller.rb +5 -0
- data/app/controllers/eventsimple/models_controller.rb +10 -0
- data/app/views/eventsimple/entities/show.html.erb +109 -0
- data/app/views/eventsimple/home/index.html.erb +0 -0
- data/app/views/eventsimple/models/show.html.erb +19 -0
- data/app/views/eventsimple/shared/_header.html.erb +26 -0
- data/app/views/eventsimple/shared/_sidebar.html.erb +19 -0
- data/app/views/eventsimple/shared/_style.html.erb +105 -0
- data/app/views/layouts/eventsimple/application.html.erb +76 -0
- data/catalog-info.yaml +11 -0
- data/config/routes.rb +11 -0
- data/eventsimple.gemspec +42 -0
- data/lib/dry_types.rb +5 -0
- data/lib/eventsimple/configuration.rb +36 -0
- data/lib/eventsimple/data_type.rb +48 -0
- data/lib/eventsimple/dispatcher.rb +17 -0
- data/lib/eventsimple/engine.rb +37 -0
- data/lib/eventsimple/entity.rb +54 -0
- data/lib/eventsimple/event.rb +189 -0
- data/lib/eventsimple/event_dispatcher.rb +93 -0
- data/lib/eventsimple/generators/install_generator.rb +42 -0
- data/lib/eventsimple/generators/outbox/install_generator.rb +31 -0
- data/lib/eventsimple/generators/outbox/templates/create_outbox_cursor.erb +13 -0
- data/lib/eventsimple/generators/templates/create_events.erb +21 -0
- data/lib/eventsimple/generators/templates/event.erb +8 -0
- data/lib/eventsimple/invalid_transition.rb +14 -0
- data/lib/eventsimple/message.rb +23 -0
- data/lib/eventsimple/metadata.rb +11 -0
- data/lib/eventsimple/metadata_type.rb +38 -0
- data/lib/eventsimple/outbox/consumer.rb +52 -0
- data/lib/eventsimple/outbox/models/cursor.rb +25 -0
- data/lib/eventsimple/reactor_worker.rb +18 -0
- data/lib/eventsimple/support/spec_helpers.rb +47 -0
- data/lib/eventsimple/version.rb +5 -0
- data/lib/eventsimple.rb +41 -0
- data/log/development.log +0 -0
- data/sonar-project.properties +4 -0
- metadata +304 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
<div class="row align-items-start">
|
2
|
+
<!-- Pane: Entity Details Start -->
|
3
|
+
<div class="col g-4 col-sm-8">
|
4
|
+
<h2><%= @model_name%>: <%= @aggregate_id %></h2>
|
5
|
+
<hr/>
|
6
|
+
|
7
|
+
<ul class="nav nav-tabs" role="tablist">
|
8
|
+
<li class="nav-item" role="presentation">
|
9
|
+
<a href="#" class="nav-link action-entity-query <%= @tab_id == 'entity' ? 'active' : '' %>" id="nav-tab-entity" data-param="t" data-value="entity">Entity</a>
|
10
|
+
</li>
|
11
|
+
<li class="nav-item" role="presentation">
|
12
|
+
<a href="#" class="nav-link action-entity-query <%= @tab_id == 'event' ? 'active' : '' %>" id="nav-tab-event" data-param="t" data-value="event">Event</a>
|
13
|
+
</li>
|
14
|
+
</ul>
|
15
|
+
|
16
|
+
<div class="tab-content pt-4 px-3">
|
17
|
+
<!-- Tab: Entity Start -->
|
18
|
+
<div id="tab-entity" class="tab-pane <%= @tab_id == 'entity' ? 'active' : '' %>" role="tabpanel" aria-labelledby="nav-tab-entity">
|
19
|
+
<p>
|
20
|
+
Shows the changes made before and after the currently selected event was applied.
|
21
|
+
</p>
|
22
|
+
<div class="table-responsive">
|
23
|
+
<table class="table table-striped">
|
24
|
+
<thead>
|
25
|
+
<tr>
|
26
|
+
<th scope="col" width="15%"></th>
|
27
|
+
<th scope="col" width="25%">Before</th>
|
28
|
+
<th scope="col" width="25%">After</th>
|
29
|
+
</tr>
|
30
|
+
</thead>
|
31
|
+
<tbody>
|
32
|
+
<% @entity_changes.each do |property| %>
|
33
|
+
<tr class="<%= property[:is_changed] ? 'table-info' : '' %>">
|
34
|
+
<th scope="row"><%= property[:label] %></th>
|
35
|
+
<td><code class="entity-property"><%= property[:historical_value].nil? ? '-' : property[:historical_value] %></code></th>
|
36
|
+
<td><code class="entity-property"><%= property[:current_value].nil? ? '-' : property[:current_value] %></code></td>
|
37
|
+
</tr>
|
38
|
+
<% end %>
|
39
|
+
</tbody>
|
40
|
+
</table>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
<!-- Tab: Entity End -->
|
44
|
+
|
45
|
+
<!-- Tab: Event Start -->
|
46
|
+
<div id="tab-event" class="tab-pane <%= @tab_id == 'event' ? 'active' : '' %>" role="tabpanel" aria-labelledby="nav-tab-event">
|
47
|
+
<p>
|
48
|
+
The properties of the <code><%= @selected_event.type %></code> event.
|
49
|
+
</p>
|
50
|
+
<table class="table">
|
51
|
+
<tbody>
|
52
|
+
<tr>
|
53
|
+
<th scope="row">Identifier</th>
|
54
|
+
<td><code class="entity-property"><%= @selected_event.id %></code></td>
|
55
|
+
</tr>
|
56
|
+
<tr>
|
57
|
+
<th scope="row">Timestamp</th>
|
58
|
+
<td><code class="entity-property"><%= @selected_event.created_at %></code></td>
|
59
|
+
</tr>
|
60
|
+
<tr>
|
61
|
+
<th scope="row" colspan="2">Data</th>
|
62
|
+
</tr>
|
63
|
+
<% if @selected_event.data.present? %>
|
64
|
+
<% @selected_event.data.attributes.each do |attr_name, attr_value| %>
|
65
|
+
<tr>
|
66
|
+
<td> <%= attr_name %></td>
|
67
|
+
<td><code class="entity-property"><%= attr_value %></code></td>
|
68
|
+
</tr>
|
69
|
+
<% end %>
|
70
|
+
<% end %>
|
71
|
+
</tr>
|
72
|
+
<tr>
|
73
|
+
<th scope="row" colspan="2">Metadata</th>
|
74
|
+
</tr>
|
75
|
+
<% @selected_event.metadata.attributes.each do |attr_name, attr_value| %>
|
76
|
+
<tr>
|
77
|
+
<td> <%= attr_name %></td>
|
78
|
+
<td><code class="entity-property">: <%= attr_value %></code></td>
|
79
|
+
</tr>
|
80
|
+
<% end %>
|
81
|
+
</tbody>
|
82
|
+
</table>
|
83
|
+
</div>
|
84
|
+
<!-- Tab: Event End -->
|
85
|
+
</div>
|
86
|
+
</div>
|
87
|
+
<!-- Pane: Entity Details End -->
|
88
|
+
|
89
|
+
<!-- Pane: Time Travel Start -->
|
90
|
+
<div class="col g-4 col-sm-4">
|
91
|
+
<h3>Time Travel</h3>
|
92
|
+
<ul class="list-group">
|
93
|
+
<% @entity_event_history.each_with_index do | event, index | %>
|
94
|
+
<% is_active_list_item = event == @selected_event ? 'list-group-item-primary' : '' %>
|
95
|
+
<% is_active_link = event == @selected_event ? 'link-dark' : 'link-dark' %>
|
96
|
+
<% event_timestamp = index.zero? ? 'Current' : "#{time_ago_in_words(event.created_at)} ago" %>
|
97
|
+
<li class="list-group-item <%= is_active_list_item %>">
|
98
|
+
<a href="#" class="text-decoration-none action-entity-query <%= is_active_link %>" data-param="e" data-value="<%= event.id %>">
|
99
|
+
<div class="d-flex w-100 justify-content-between">
|
100
|
+
<span title="<%= event.type %>" style="width: 75%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;"><%= event.type.demodulize %></span>
|
101
|
+
<small><%= event_timestamp %></small>
|
102
|
+
</div>
|
103
|
+
</a>
|
104
|
+
</li>
|
105
|
+
<% end %>
|
106
|
+
</ul>
|
107
|
+
</div>
|
108
|
+
<!-- Pane: Time Travel End -->
|
109
|
+
</div>
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
<table class="table table-striped">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th scope="col" width="15%">Time</th>
|
6
|
+
<th scope="col" width="30%">Identifier</th>
|
7
|
+
<th scope="col" width="65%">Event</th>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<tbody>
|
11
|
+
<% @latest_entities.each do |event| %>
|
12
|
+
<tr>
|
13
|
+
<td scope="row"><%= "#{time_ago_in_words(event.created_at)} ago" %></td>
|
14
|
+
<td><%= link_to event.aggregate_id, model_entity_path(@model_name, event.aggregate_id) %></td>
|
15
|
+
<td><%= event.type %></td>
|
16
|
+
</tr>
|
17
|
+
<% end %>
|
18
|
+
</tbody>
|
19
|
+
</table>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<!-- Header Search Start -->
|
2
|
+
<header class="container mb-4">
|
3
|
+
<div class="row">
|
4
|
+
<div class="col col-sm-3">
|
5
|
+
<select id="form-search-model-name" class="form-select" aria-label="Model select" onchange="document.location.href=this.value" required>
|
6
|
+
<option value="">Choose event source model</option>
|
7
|
+
<% event_class_names.each do |klass| %>
|
8
|
+
<% is_active = klass === @model_name ? 'selected' : '' %>
|
9
|
+
<option value="<%= model_path(klass) %>" <%= is_active %>><%= klass %></option>
|
10
|
+
<% end %>
|
11
|
+
</select>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<% if @model_name %>
|
15
|
+
<div class="col col-sm-7">
|
16
|
+
<%= form_with url: model_entity_path(@model_name, ''), id: 'model-search' do |f| %>
|
17
|
+
<%= f.search_field :event_id, class: 'form-control', placeholder: 'Canonical identifier', value: @aggregate_id, required: true, aria: { label: 'Entity canonical identifier' } %>
|
18
|
+
</div>
|
19
|
+
<div class="col col-sm-2">
|
20
|
+
<%= f.submit 'Search', class: 'btn btn-primary' %>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
23
|
+
<% end %>
|
24
|
+
</div>
|
25
|
+
</header>
|
26
|
+
<!-- Header Search End -->
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<!-- Sidebar Nav Start -->
|
2
|
+
<nav class="d-flex flex-column flex-shrink-0 p-3 text-white bg-dark">
|
3
|
+
<%= link_to root_path, class: 'd-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none' do %>
|
4
|
+
<span class="fs-4">Eventsimple</span>
|
5
|
+
<% end %>
|
6
|
+
<hr>
|
7
|
+
<ul class="nav nav-pills flex-column mb-auto">
|
8
|
+
<% event_class_names.each do |klass| %>
|
9
|
+
<% is_active = klass === @model_name ? 'active text-white' : 'text-white' %>
|
10
|
+
<li class="nav-item text-white overflow-hidden" style="width: 100%;">
|
11
|
+
<%= link_to model_path(klass), class: "nav-link #{is_active}", style: "overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" do %>
|
12
|
+
<i class="bi bi-table"></i>
|
13
|
+
<%= klass %>
|
14
|
+
<% end %>
|
15
|
+
</li>
|
16
|
+
<% end %>
|
17
|
+
</ul>
|
18
|
+
</nav>
|
19
|
+
<!-- Sidebar Nav End -->
|
@@ -0,0 +1,105 @@
|
|
1
|
+
<style>
|
2
|
+
// Structure
|
3
|
+
body {
|
4
|
+
min-height: 100vh;
|
5
|
+
min-height: -webkit-fill-available;
|
6
|
+
}
|
7
|
+
|
8
|
+
html {
|
9
|
+
height: -webkit-fill-available;
|
10
|
+
}
|
11
|
+
|
12
|
+
#page {
|
13
|
+
display: flex;
|
14
|
+
flex-wrap: nowrap;
|
15
|
+
height: 100vh;
|
16
|
+
height: -webkit-fill-available;
|
17
|
+
max-height: 100vh;
|
18
|
+
overflow-x: auto;
|
19
|
+
overflow-y: hidden;
|
20
|
+
}
|
21
|
+
|
22
|
+
nav {
|
23
|
+
width: 20%;
|
24
|
+
}
|
25
|
+
|
26
|
+
main {
|
27
|
+
flex-shrink: 0;
|
28
|
+
height: 100vh;
|
29
|
+
width: 80%;
|
30
|
+
overflow-y: auto;
|
31
|
+
}
|
32
|
+
|
33
|
+
// Sidebar Nav
|
34
|
+
|
35
|
+
.bi {
|
36
|
+
vertical-align: -.125em;
|
37
|
+
pointer-events: none;
|
38
|
+
fill: currentColor;
|
39
|
+
}
|
40
|
+
|
41
|
+
.dropdown-toggle { outline: 0; }
|
42
|
+
|
43
|
+
.nav-flush .nav-link {
|
44
|
+
border-radius: 0;
|
45
|
+
}
|
46
|
+
|
47
|
+
.btn-toggle {
|
48
|
+
display: inline-flex;
|
49
|
+
align-items: center;
|
50
|
+
padding: .25rem .5rem;
|
51
|
+
font-weight: 600;
|
52
|
+
color: rgba(0, 0, 0, .65);
|
53
|
+
background-color: transparent;
|
54
|
+
border: 0;
|
55
|
+
}
|
56
|
+
.btn-toggle:hover,
|
57
|
+
.btn-toggle:focus {
|
58
|
+
color: rgba(0, 0, 0, .85);
|
59
|
+
background-color: #d2f4ea;
|
60
|
+
}
|
61
|
+
|
62
|
+
.btn-toggle::before {
|
63
|
+
width: 1.25em;
|
64
|
+
line-height: 0;
|
65
|
+
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
66
|
+
transition: transform .35s ease;
|
67
|
+
transform-origin: .5em 50%;
|
68
|
+
}
|
69
|
+
|
70
|
+
.btn-toggle[aria-expanded="true"] {
|
71
|
+
color: rgba(0, 0, 0, .85);
|
72
|
+
}
|
73
|
+
.btn-toggle[aria-expanded="true"]::before {
|
74
|
+
transform: rotate(90deg);
|
75
|
+
}
|
76
|
+
|
77
|
+
.btn-toggle-nav a {
|
78
|
+
display: inline-flex;
|
79
|
+
padding: .1875rem .5rem;
|
80
|
+
margin-top: .125rem;
|
81
|
+
margin-left: 1.25rem;
|
82
|
+
text-decoration: none;
|
83
|
+
}
|
84
|
+
.btn-toggle-nav a:hover,
|
85
|
+
.btn-toggle-nav a:focus {
|
86
|
+
background-color: #d2f4ea;
|
87
|
+
}
|
88
|
+
|
89
|
+
.scrollarea {
|
90
|
+
overflow-y: auto;
|
91
|
+
}
|
92
|
+
|
93
|
+
.fw-semibold { font-weight: 600; }
|
94
|
+
.lh-tight { line-height: 1.25; }
|
95
|
+
|
96
|
+
// Styles
|
97
|
+
|
98
|
+
code {
|
99
|
+
color: deeppink;
|
100
|
+
}
|
101
|
+
|
102
|
+
code.entity-property {
|
103
|
+
color: #333;
|
104
|
+
}
|
105
|
+
</style>
|
@@ -0,0 +1,76 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6
|
+
<meta name="description" content="">
|
7
|
+
<title>Eventsimple</title>
|
8
|
+
<%= csrf_meta_tags %>
|
9
|
+
<%= csp_meta_tag %>
|
10
|
+
|
11
|
+
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css",
|
12
|
+
media: "all",
|
13
|
+
integrity: 'sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65',
|
14
|
+
crossorigin: 'anonymous'
|
15
|
+
%>
|
16
|
+
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css",
|
17
|
+
media: "all",
|
18
|
+
crossorigin: 'anonymous'
|
19
|
+
%>
|
20
|
+
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js",
|
21
|
+
integrity: "sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V",
|
22
|
+
crossorigin: "anonymous"
|
23
|
+
%>
|
24
|
+
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js",
|
25
|
+
crossorigin: "anonymous"
|
26
|
+
%>
|
27
|
+
|
28
|
+
<%= render partial: 'eventsimple/shared/style' %>
|
29
|
+
</head>
|
30
|
+
<body>
|
31
|
+
<div id="page">
|
32
|
+
<%= render partial: 'eventsimple/shared/sidebar' %>
|
33
|
+
|
34
|
+
<!-- Main Container Start -->
|
35
|
+
<main class="pt-4 px-2">
|
36
|
+
<%= render partial: 'eventsimple/shared/header' %>
|
37
|
+
|
38
|
+
<!-- Content Start -->
|
39
|
+
<article class="container">
|
40
|
+
<% if @error_message.present? %>
|
41
|
+
<div class="alert alert-warning d-flex" role="alert">
|
42
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
43
|
+
<div>
|
44
|
+
<%= @error_message %>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
<% else %>
|
48
|
+
<%= yield %>
|
49
|
+
<% end %>
|
50
|
+
</article>
|
51
|
+
<!-- Content End -->
|
52
|
+
|
53
|
+
</main>
|
54
|
+
<!-- Main Container End -->
|
55
|
+
|
56
|
+
<script>
|
57
|
+
$( document ).ready(() => {
|
58
|
+
$('.action-entity-query').click((event) => {
|
59
|
+
const link = $(event.currentTarget);
|
60
|
+
const { param, value } = link.data();
|
61
|
+
const query = new URLSearchParams(window.location.search)
|
62
|
+
query.set(param, value);
|
63
|
+
link.attr("href",window.location.pathname + '?' + query.toString());
|
64
|
+
});
|
65
|
+
|
66
|
+
$('#model-search').on('submit', (event) => {
|
67
|
+
event.preventDefault();
|
68
|
+
const form = $(event.currentTarget);
|
69
|
+
window.location = form.attr('action') + form.children('#event_id').val();
|
70
|
+
});
|
71
|
+
});
|
72
|
+
</script>
|
73
|
+
|
74
|
+
</div>
|
75
|
+
</body>
|
76
|
+
</html>
|
data/catalog-info.yaml
ADDED
data/config/routes.rb
ADDED
data/eventsimple.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/eventsimple/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'eventsimple'
|
7
|
+
spec.version = Eventsimple::VERSION
|
8
|
+
spec.authors = ['Zulfiqar Ali']
|
9
|
+
spec.email = ['zulfiqar@wealthsimple.com']
|
10
|
+
|
11
|
+
spec.summary = 'Event driven toolkit using Rails and Sidekiq'
|
12
|
+
spec.description = 'Event driven architecture using Rails and Sidekiq'
|
13
|
+
spec.homepage = 'https://github.com/wealthsimple/eventsimple'
|
14
|
+
spec.required_ruby_version = ">= 3.2.0"
|
15
|
+
|
16
|
+
spec.metadata['changelog_uri'] = "https://github.com/wealthsimple/eventsimple/blob/main/CHANGELOG.md"
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
20
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git))})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'dry-struct', '~> 1.6'
|
27
|
+
spec.add_runtime_dependency 'dry-types', '~> 1.7'
|
28
|
+
spec.add_runtime_dependency 'pg', '~> 1.4'
|
29
|
+
spec.add_runtime_dependency 'rails', '~> 7.0'
|
30
|
+
spec.add_runtime_dependency 'retriable', '~> 3.1'
|
31
|
+
spec.add_runtime_dependency 'sidekiq', '~> 7.0'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'bundle-audit'
|
34
|
+
spec.add_development_dependency 'fuubar'
|
35
|
+
spec.add_development_dependency 'git'
|
36
|
+
spec.add_development_dependency 'guard-rspec'
|
37
|
+
spec.add_development_dependency 'parse_a_changelog'
|
38
|
+
spec.add_development_dependency 'pry'
|
39
|
+
spec.add_development_dependency 'puma'
|
40
|
+
spec.add_development_dependency 'rspec-rails'
|
41
|
+
spec.add_development_dependency 'ws-style'
|
42
|
+
end
|
data/lib/dry_types.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Eventsimple
|
4
|
+
class Configuration
|
5
|
+
attr_reader :max_concurrency_retries, :dispatchers
|
6
|
+
attr_writer :metadata_klass
|
7
|
+
|
8
|
+
attr_accessor :ui_visible_models
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@dispatchers = []
|
12
|
+
@max_concurrency_retries = 2
|
13
|
+
@metadata_klass = 'Eventsimple::Metadata'
|
14
|
+
|
15
|
+
@ui_visible_models = [] # internal use only
|
16
|
+
end
|
17
|
+
|
18
|
+
def max_concurrency_retries=(value)
|
19
|
+
unless value.is_a?(Integer) && value.positive?
|
20
|
+
raise ArgumentError, 'max_concurrency_retries must be a positive integer'
|
21
|
+
end
|
22
|
+
|
23
|
+
@max_concurrency_retries = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def dispatchers=(value)
|
27
|
+
raise ArgumentError, 'dispatchers must be an array' unless value.is_a?(Array)
|
28
|
+
|
29
|
+
@dispatchers = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def metadata_klass
|
33
|
+
@klass ||= @metadata_klass.constantize # rubocop:disable Naming/MemoizedInstanceVariableName
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Eventsimple
|
4
|
+
class DataType < ActiveModel::Type::Value
|
5
|
+
def initialize(event_klass)
|
6
|
+
@event_klass = event_klass
|
7
|
+
super()
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :event_klass
|
11
|
+
|
12
|
+
def type
|
13
|
+
:data_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def cast_value(value)
|
17
|
+
case value
|
18
|
+
when String
|
19
|
+
decoded = ActiveSupport::JSON.decode(value)
|
20
|
+
return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
|
21
|
+
|
22
|
+
decoded
|
23
|
+
when Hash
|
24
|
+
return event_klass::Message.new(value) if event_klass.const_defined?(:Message)
|
25
|
+
|
26
|
+
value
|
27
|
+
when event_klass::Message
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize(value)
|
33
|
+
case value
|
34
|
+
when Hash, event_klass::Message
|
35
|
+
ActiveSupport::JSON.encode(value)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def deserialize(value)
|
42
|
+
decoded = ActiveSupport::JSON.decode(value)
|
43
|
+
return event_klass::Message.new(decoded) if event_klass.const_defined?(:Message)
|
44
|
+
|
45
|
+
decoded
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Eventsimple
|
2
|
+
class Dispatcher
|
3
|
+
def self.on(*events, sync: [], async: [])
|
4
|
+
# Register Reactors to Events.
|
5
|
+
# * Reactors registered with `sync` will be synced synchronously
|
6
|
+
# * Reactors registered with `async` will be synced asynchronously via a Sidekiq Job
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# on BaseEvent, sync: LogEvent, async: TrackEvent
|
11
|
+
# on PledgeCancelled, PaymentFailed, async: [NotifyAdmin, CreateTask]
|
12
|
+
# on [PledgeCancelled, PaymentFailed], async: [NotifyAdmin, CreateTask]
|
13
|
+
#
|
14
|
+
EventDispatcher.rules.register(events: events.flatten, sync: Array(sync), async: Array(async))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module Eventsimple
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Eventsimple
|
6
|
+
|
7
|
+
config.generators do |g|
|
8
|
+
g.test_framework :rspec
|
9
|
+
g.helper false
|
10
|
+
g.view_specs false
|
11
|
+
end
|
12
|
+
|
13
|
+
config.after_initialize do
|
14
|
+
dispatchers = Eventsimple.configuration.dispatchers.map(&:constantize)
|
15
|
+
|
16
|
+
unless dispatchers.all? { |dispatcher| dispatcher.superclass == Eventsimple::Dispatcher }
|
17
|
+
raise ArgumentError, 'dispatchers must inherit from Eventsimple::Dispatcher'
|
18
|
+
end
|
19
|
+
|
20
|
+
retry_intervals = Array.new(Eventsimple.configuration.max_concurrency_retries) { 0 }
|
21
|
+
|
22
|
+
Retriable.configure do |c|
|
23
|
+
c.contexts[:reactor] = {
|
24
|
+
tries: 7,
|
25
|
+
base_interval: 1.0,
|
26
|
+
multiplier: 1.0,
|
27
|
+
rand_factor: 0.0,
|
28
|
+
on: ActiveRecord::RecordNotFound,
|
29
|
+
}
|
30
|
+
c.contexts[:optimistic_locking] = {
|
31
|
+
intervals: retry_intervals,
|
32
|
+
on: ActiveRecord::StaleObjectError,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Eventsimple
|
2
|
+
module Entity
|
3
|
+
DEFAULT_IGNORE_PROPS = %w[id lock_version].freeze
|
4
|
+
|
5
|
+
def event_driven_by(event_klass, aggregate_id:)
|
6
|
+
has_many :events, class_name: event_klass.name.to_s,
|
7
|
+
foreign_key: :aggregate_id,
|
8
|
+
primary_key: aggregate_id,
|
9
|
+
dependent: :delete_all,
|
10
|
+
inverse_of: model_name.element.to_sym,
|
11
|
+
autosave: false,
|
12
|
+
validate: false
|
13
|
+
|
14
|
+
class_attribute :ignored_for_projection, default: []
|
15
|
+
|
16
|
+
# disable automatic timestamp updates
|
17
|
+
self.record_timestamps = false
|
18
|
+
|
19
|
+
Eventsimple.configuration.ui_visible_models |= [self]
|
20
|
+
|
21
|
+
include InstanceMethods
|
22
|
+
extend ClassMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
def projection_matches_events?
|
27
|
+
reprojected = self.class.find(id).reproject
|
28
|
+
|
29
|
+
attributes == reprojected.attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def reproject(at: nil)
|
33
|
+
event_history = at ? events.where('created_at <= ?', at).load : events.load
|
34
|
+
ignore_props = (DEFAULT_IGNORE_PROPS + ignored_for_projection).map(&:to_s)
|
35
|
+
assign_attributes(self.class.column_defaults.except(*ignore_props))
|
36
|
+
|
37
|
+
event_history.each do |event|
|
38
|
+
event.apply(self)
|
39
|
+
event.apply_timestamps(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def event_class
|
48
|
+
reflect_on_all_associations(:has_many).find { |association|
|
49
|
+
association.name == :events
|
50
|
+
}.klass
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|