ruby_event_store-browser 2.19.2 → 3.1.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 +4 -4
- data/README.md +1 -1
- data/lib/ruby_event_store/browser/app.rb +190 -86
- data/lib/ruby_event_store/browser/extension_context.rb +22 -0
- data/lib/ruby_event_store/browser/get_events_from_stream.rb +50 -93
- data/lib/ruby_event_store/browser/get_events_from_streams.rb +98 -0
- data/lib/ruby_event_store/browser/layout.rb +55 -0
- data/lib/ruby_event_store/browser/renderer.rb +64 -0
- data/lib/ruby_event_store/browser/urls.rb +35 -39
- data/lib/ruby_event_store/browser/version.rb +1 -1
- data/lib/ruby_event_store/browser/views/_clipboard_icon.html.erb +1 -0
- data/lib/ruby_event_store/browser/views/_clock_icon.html.erb +1 -0
- data/lib/ruby_event_store/browser/views/_json_tree.html.erb +1 -0
- data/lib/ruby_event_store/browser/views/_raw_section.html.erb +9 -0
- data/lib/ruby_event_store/browser/views/_timestamp.html.erb +7 -0
- data/lib/ruby_event_store/browser/views/events/show.html.erb +85 -0
- data/lib/ruby_event_store/browser/views/layout.html.erb +72 -0
- data/lib/ruby_event_store/browser/views/not_found.html.erb +3 -0
- data/lib/ruby_event_store/browser/views/streams/show.html.erb +74 -0
- data/lib/ruby_event_store/browser/views/swimlane/_rows.html.erb +20 -0
- data/lib/ruby_event_store/browser/views/swimlane/show.html.erb +55 -0
- data/lib/ruby_event_store/browser.rb +17 -5
- data/public/ruby_event_store_browser.css +2 -1
- data/public/ruby_event_store_browser.js +287 -1
- data/public/stimulus-3.2.2.js +11 -0
- metadata +21 -11
- data/lib/ruby_event_store/browser/gem_source.rb +0 -32
- data/lib/ruby_event_store/browser/get_event.rb +0 -37
- data/lib/ruby_event_store/browser/get_stream.rb +0 -29
- data/lib/ruby_event_store/browser/json_api_event.rb +0 -66
- data/lib/ruby_event_store/browser/json_api_stream.rb +0 -34
- data/public/bootstrap.js +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c34bdfb8bf1ac7bd5ee801add4be05d53e9698f9e62b7dd55ba822ab5210b10
|
|
4
|
+
data.tar.gz: 03ff99bad816c753b0b69554e19233467842ff74d0df7e21e1a36645550b9336
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 851f7a67f73ac05f96b6ac76ce73690783eed6bc71b830b2740a6b529fb4f47396ba2e7490f969648c9fede5c54a975f7e39c94fe6f4231c09f8b803456a7af9
|
|
7
|
+
data.tar.gz: 7a9c17aa335a429247c81561ee301d26af132952b52113a0c439a2b5afbfc5b1c38b77a5154b768b75727f55dfdd8ef1592f3fab840760dde5982e137e31bef0
|
data/README.md
CHANGED
|
@@ -6,4 +6,4 @@ Find out more at [https://railseventstore.org](https://railseventstore.org/)
|
|
|
6
6
|
|
|
7
7
|
## UI Dev mode
|
|
8
8
|
|
|
9
|
-
`make
|
|
9
|
+
`make build` compiles the CSS with Tailwind and vendors the pinned Stimulus runtime. `make dev` serves the standalone browser seeded with some events on http://localhost:9393
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../browser"
|
|
4
4
|
require "rack"
|
|
5
|
-
require "
|
|
5
|
+
require "uri"
|
|
6
6
|
require "json"
|
|
7
7
|
|
|
8
8
|
module RubyEventStore
|
|
@@ -14,37 +14,42 @@ module RubyEventStore
|
|
|
14
14
|
path: nil,
|
|
15
15
|
api_url: nil,
|
|
16
16
|
environment: nil,
|
|
17
|
-
related_streams_query: DEFAULT_RELATED_STREAMS_QUERY
|
|
17
|
+
related_streams_query: DEFAULT_RELATED_STREAMS_QUERY,
|
|
18
|
+
extensions: [],
|
|
19
|
+
views_root: nil
|
|
18
20
|
)
|
|
19
21
|
warn(<<~WARN) if environment
|
|
20
|
-
|
|
22
|
+
Passing :environment to RubyEventStore::Browser::App.for is deprecated.
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
This option is no-op, has no effect and will be removed in next major release.
|
|
25
|
+
WARN
|
|
24
26
|
warn(<<~WARN) if host
|
|
25
|
-
|
|
27
|
+
Passing :host to RubyEventStore::Browser::App.for is deprecated.
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
This option will be removed in next major release.
|
|
30
|
+
|
|
31
|
+
Host and mount points are correctly recognized from Rack environment
|
|
32
|
+
and this option is redundant.
|
|
33
|
+
WARN
|
|
32
34
|
warn(<<~WARN) if path
|
|
33
|
-
|
|
35
|
+
Passing :path to RubyEventStore::Browser::App.for is deprecated.
|
|
36
|
+
|
|
37
|
+
This option will be removed in next major release.
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
Host and mount points are correctly recognized from Rack environment
|
|
40
|
+
and this option is redundant.
|
|
41
|
+
WARN
|
|
42
|
+
warn(<<~WARN) if api_url
|
|
43
|
+
Passing :api_url to RubyEventStore::Browser::App.for is deprecated.
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
WARN
|
|
45
|
+
This option is no-op and will be removed in next major release.
|
|
46
|
+
WARN
|
|
40
47
|
|
|
41
48
|
Rack::Builder.new do
|
|
42
49
|
use Rack::Static,
|
|
43
50
|
urls:
|
|
44
51
|
%w[
|
|
45
|
-
|
|
46
|
-
ruby_event_store_browser.css
|
|
47
|
-
ruby_event_store_browser.js
|
|
52
|
+
stimulus-3.2.2.js
|
|
48
53
|
android-chrome-192x192.png
|
|
49
54
|
android-chrome-512x512.png
|
|
50
55
|
apple-touch-icon.png
|
|
@@ -57,109 +62,208 @@ module RubyEventStore
|
|
|
57
62
|
mstile-310x150.png
|
|
58
63
|
mstile-310x310.png
|
|
59
64
|
safari-pinned-tab.svg
|
|
60
|
-
|
|
65
|
+
ruby_event_store_browser.js
|
|
66
|
+
ruby_event_store_browser.css
|
|
67
|
+
].map { |f| ["/#{f}", f] }.to_h.merge(
|
|
68
|
+
"/#{BROWSER_JS}" => "ruby_event_store_browser.js",
|
|
69
|
+
"/#{BROWSER_CSS}" => "ruby_event_store_browser.css",
|
|
70
|
+
),
|
|
61
71
|
root: "#{__dir__}/../../../public"
|
|
72
|
+
extensions.each_with_index do |extension, index|
|
|
73
|
+
mappings = App.extension_asset_mappings(extension, index)
|
|
74
|
+
use Rack::Static, urls: mappings, root: extension.assets_root unless mappings.empty?
|
|
75
|
+
end
|
|
62
76
|
run App.new(
|
|
63
77
|
event_store_locator: event_store_locator,
|
|
64
78
|
related_streams_query: related_streams_query,
|
|
65
79
|
host: host,
|
|
66
80
|
root_path: path,
|
|
67
|
-
|
|
81
|
+
extensions: extensions,
|
|
82
|
+
views_root: views_root,
|
|
68
83
|
)
|
|
69
84
|
end
|
|
70
85
|
end
|
|
71
86
|
|
|
72
|
-
def
|
|
87
|
+
def self.extension_asset_mappings(extension, index)
|
|
88
|
+
return {} unless extension.respond_to?(:assets_root)
|
|
89
|
+
|
|
90
|
+
Dir
|
|
91
|
+
.glob("**/*", base: extension.assets_root)
|
|
92
|
+
.to_h { |relative_path| ["/extension_assets/#{index}/#{relative_path}", relative_path] }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def initialize(event_store_locator:, related_streams_query:, host:, root_path:, extensions: [], views_root: nil)
|
|
73
96
|
@event_store_locator = event_store_locator
|
|
74
97
|
@related_streams_query = related_streams_query
|
|
75
|
-
@
|
|
98
|
+
@extensions = extensions
|
|
99
|
+
@views_root = views_root
|
|
100
|
+
@routing = Urls.from_configuration(host, root_path)
|
|
76
101
|
end
|
|
77
102
|
|
|
78
103
|
def call(env)
|
|
104
|
+
request = Rack::Request.new(env)
|
|
79
105
|
router = Router.new(routing)
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
|
|
107
|
+
router.add_route("GET", "/") do |_, urls|
|
|
108
|
+
[302, { "location" => urls.stream_url(SERIALIZED_GLOBAL_STREAM_NAME) }, []]
|
|
82
109
|
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
|
|
111
|
+
router.add_route("GET", "/streams/:stream_name") do |params, urls|
|
|
112
|
+
stream_name = params.fetch("stream_name")
|
|
113
|
+
reader = GetEventsFromStream.new(event_store: event_store, stream_name: stream_name, page: params["page"])
|
|
114
|
+
layout.render(
|
|
115
|
+
"streams/show",
|
|
116
|
+
urls: urls,
|
|
117
|
+
stream_name: stream_name,
|
|
118
|
+
title: "Stream #{stream_name}",
|
|
119
|
+
events: reader.events,
|
|
120
|
+
pagination:
|
|
121
|
+
reader.pagination.transform_values { |cursor| urls.stream_page_url(stream_name, cursor, reader.count) },
|
|
122
|
+
related_streams: related_streams_query.call(stream_name),
|
|
123
|
+
extension_links: stream_extension_links(stream_name, urls),
|
|
124
|
+
)
|
|
89
125
|
end
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
|
|
127
|
+
router.add_route("GET", "/events/:event_id") do |params, urls|
|
|
128
|
+
event = event_store.read.event!(params.fetch("event_id"))
|
|
129
|
+
metadata = format_event_metadata(event)
|
|
130
|
+
parent_event =
|
|
131
|
+
event_store.read.event(event.metadata.fetch(:causation_id)) if event.metadata.key?(:causation_id)
|
|
132
|
+
|
|
133
|
+
layout.render(
|
|
134
|
+
"events/show",
|
|
135
|
+
urls: urls,
|
|
136
|
+
event: event,
|
|
137
|
+
title: "Event #{event.event_id}",
|
|
138
|
+
metadata: metadata,
|
|
139
|
+
streams: event_store.streams_of(event.event_id).map(&:name).sort,
|
|
140
|
+
parent_event: parent_event,
|
|
141
|
+
caused_by: event_store.read.stream("$by_causation_id_#{event.event_id}").backward.limit(PAGE_SIZE).to_a,
|
|
142
|
+
extension_links: event_extension_links(event, urls),
|
|
143
|
+
)
|
|
97
144
|
end
|
|
98
145
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
146
|
+
router.add_route("GET", "/swimlane") do |params, urls|
|
|
147
|
+
stream_names, sort = swimlane_params(params)
|
|
148
|
+
reader = GetEventsFromStreams.new(event_store: event_store, stream_names: stream_names, sort: sort)
|
|
149
|
+
missing_stream_names = reader.missing_stream_names
|
|
150
|
+
stream_names -= missing_stream_names
|
|
151
|
+
layout.render(
|
|
152
|
+
"swimlane/show",
|
|
153
|
+
urls: urls,
|
|
154
|
+
stream_names: stream_names,
|
|
155
|
+
missing_stream_names: missing_stream_names,
|
|
156
|
+
title: "Swimlane #{stream_names.join(', ')}",
|
|
157
|
+
events: reader.events,
|
|
158
|
+
sort: sort,
|
|
159
|
+
more_url: (urls.swimlane_more_url(stream_names, reader.next_cursor, sort) if reader.more?),
|
|
160
|
+
)
|
|
111
161
|
end
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
162
|
+
|
|
163
|
+
router.add_route("GET", "/swimlane/more") do |params, urls|
|
|
164
|
+
stream_names, sort = swimlane_params(params)
|
|
165
|
+
reader =
|
|
166
|
+
GetEventsFromStreams.new(
|
|
167
|
+
event_store: event_store,
|
|
168
|
+
stream_names: stream_names,
|
|
169
|
+
cursor: params["cursor"],
|
|
170
|
+
sort: sort,
|
|
171
|
+
)
|
|
172
|
+
json(
|
|
173
|
+
html:
|
|
174
|
+
layout.render_partial("swimlane/_rows", urls: urls, stream_names: stream_names, events: reader.events, sort: sort),
|
|
175
|
+
more_url: (urls.swimlane_more_url(stream_names, reader.next_cursor, sort) if reader.more?),
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
extensions
|
|
180
|
+
.select { |extension| extension.respond_to?(:register_routes) }
|
|
181
|
+
.each { |extension| extension.register_routes(router, extension_context(extension)) }
|
|
182
|
+
|
|
183
|
+
router.handle(request)
|
|
184
|
+
rescue EventNotFound
|
|
185
|
+
layout.not_found(routing.with_request(request), message: "There's no event with given ID")
|
|
186
|
+
rescue Router::NoMatch
|
|
187
|
+
layout.not_found(routing.with_request(request), message: "Page not found")
|
|
115
188
|
end
|
|
116
189
|
|
|
117
190
|
private
|
|
118
191
|
|
|
119
|
-
attr_reader :event_store_locator, :related_streams_query, :routing
|
|
192
|
+
attr_reader :event_store_locator, :related_streams_query, :routing, :extensions, :views_root
|
|
120
193
|
|
|
121
194
|
def event_store
|
|
122
195
|
event_store_locator.call
|
|
123
196
|
end
|
|
124
197
|
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
<!DOCTYPE html>
|
|
128
|
-
<html>
|
|
129
|
-
<head>
|
|
130
|
-
<title>RubyEventStore::Browser</title>
|
|
131
|
-
<link type="text/css" rel="stylesheet" href="<%= browser_css_src %>">
|
|
132
|
-
<meta name="ruby-event-store-browser-settings" content="<%= Rack::Utils.escape_html(JSON.dump(initial_data)) %>">
|
|
133
|
-
<meta name="robots" content="noindex, nofollow">
|
|
134
|
-
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
|
135
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
|
136
|
-
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
|
137
|
-
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
|
138
|
-
<meta name="msapplication-TileColor" content="#da532c">
|
|
139
|
-
<meta name="theme-color" content="#ffffff">
|
|
140
|
-
</head>
|
|
141
|
-
<body>
|
|
142
|
-
<script type="text/javascript" src="<%= browser_js_src %>"></script>
|
|
143
|
-
<script type="text/javascript" src="<%= bootstrap_js_src %>"></script>
|
|
144
|
-
</body>
|
|
145
|
-
</html>
|
|
146
|
-
HTML
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def not_found
|
|
150
|
-
[404, {}, []]
|
|
198
|
+
def extension_context(extension)
|
|
199
|
+
ExtensionContext.new(event_store, extension_layout(extension))
|
|
151
200
|
end
|
|
152
201
|
|
|
153
|
-
def
|
|
154
|
-
|
|
202
|
+
def extension_layout(extension)
|
|
203
|
+
extension.respond_to?(:views_root) ? layout.with_views_root(extension.views_root) : layout
|
|
155
204
|
end
|
|
156
205
|
|
|
157
|
-
def
|
|
158
|
-
|
|
206
|
+
def layout
|
|
207
|
+
Layout.new(
|
|
208
|
+
method(:extension_stylesheets),
|
|
209
|
+
method(:extension_scripts),
|
|
210
|
+
method(:extension_nav_links),
|
|
211
|
+
views_roots: [views_root].compact,
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def stream_extension_links(stream_name, urls)
|
|
216
|
+
extensions
|
|
217
|
+
.select { |extension| extension.respond_to?(:stream_links) }
|
|
218
|
+
.flat_map { |extension| extension.stream_links(stream_name, urls) }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def event_extension_links(event, urls)
|
|
222
|
+
extensions
|
|
223
|
+
.select { |extension| extension.respond_to?(:event_links) }
|
|
224
|
+
.flat_map { |extension| extension.event_links(event, urls) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def extension_nav_links(urls)
|
|
228
|
+
extensions
|
|
229
|
+
.select { |extension| extension.respond_to?(:nav_links) }
|
|
230
|
+
.flat_map { |extension| extension.nav_links(urls) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def extension_stylesheets(urls)
|
|
234
|
+
extension_asset_urls(urls, ".css")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def extension_scripts(urls)
|
|
238
|
+
extension_asset_urls(urls, ".js")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def extension_asset_urls(urls, extname)
|
|
242
|
+
extensions.each_with_index.flat_map do |extension, index|
|
|
243
|
+
App
|
|
244
|
+
.extension_asset_mappings(extension, index)
|
|
245
|
+
.keys
|
|
246
|
+
.select { |path| File.extname(path) == extname }
|
|
247
|
+
.map { |path| "#{urls.app_url}#{path}" }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def format_event_metadata(event)
|
|
252
|
+
event.metadata.to_h.tap do |metadata|
|
|
253
|
+
%i[timestamp valid_at].each do |key|
|
|
254
|
+
metadata[key] = metadata.fetch(key).iso8601(RubyEventStore::TIMESTAMP_PRECISION) if metadata.key?(key)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def json(body)
|
|
260
|
+
[200, { "content-type" => "application/json" }, [JSON.generate(body)]]
|
|
159
261
|
end
|
|
160
262
|
|
|
161
|
-
def
|
|
162
|
-
|
|
263
|
+
def swimlane_params(params)
|
|
264
|
+
stream_names = Array(params["streams"]).reject { |name| name.nil? || name.empty? }.uniq
|
|
265
|
+
sort = ("as_of" if params["sort"] == "as_of")
|
|
266
|
+
[stream_names, sort]
|
|
163
267
|
end
|
|
164
268
|
end
|
|
165
269
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyEventStore
|
|
4
|
+
module Browser
|
|
5
|
+
class ExtensionContext
|
|
6
|
+
attr_reader :event_store
|
|
7
|
+
|
|
8
|
+
def initialize(event_store, layout)
|
|
9
|
+
@event_store = event_store
|
|
10
|
+
@layout = layout
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(template, urls:, title: nil, **locals)
|
|
14
|
+
@layout.render(template, urls: urls, title: title, **locals)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def not_found(urls, message: "Page not found")
|
|
18
|
+
@layout.not_found(urls, message: message)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -2,115 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyEventStore
|
|
4
4
|
module Browser
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@routing = routing
|
|
11
|
-
@stream_name = stream_name
|
|
12
|
-
@page = page || {}
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def to_h
|
|
16
|
-
{ data: events.map { |e| JsonApiEvent.new(e, nil).to_h }, links: links }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
attr_reader :event_store, :routing, :stream_name, :page
|
|
5
|
+
GetEventsFromStream =
|
|
6
|
+
Struct.new(:event_store, :stream_name, :page, keyword_init: true) do
|
|
7
|
+
def initialize(event_store:, stream_name:, page:)
|
|
8
|
+
super(event_store: event_store, stream_name: stream_name, page: page || {})
|
|
9
|
+
end
|
|
22
10
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
11
|
+
def events
|
|
12
|
+
@events ||=
|
|
13
|
+
case direction
|
|
14
|
+
when :forward
|
|
15
|
+
read(event_store.read, position).reverse
|
|
16
|
+
when :backward
|
|
17
|
+
read(event_store.read.backward, position)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
def pagination
|
|
22
|
+
found = events
|
|
35
23
|
{}.tap do |h|
|
|
36
|
-
if prev_event?
|
|
37
|
-
h[:prev] =
|
|
38
|
-
h[:first] =
|
|
24
|
+
if prev_event?(found)
|
|
25
|
+
h[:prev] = { position: found.first.event_id, direction: :forward }
|
|
26
|
+
h[:first] = { position: :head, direction: :backward }
|
|
39
27
|
end
|
|
40
28
|
|
|
41
|
-
if next_event?
|
|
42
|
-
h[:next] =
|
|
43
|
-
h[:last] =
|
|
29
|
+
if next_event?(found)
|
|
30
|
+
h[:next] = { position: found.last.event_id, direction: :backward }
|
|
31
|
+
h[:last] = { position: :head, direction: :forward }
|
|
44
32
|
end
|
|
45
33
|
end
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def events_forward(position)
|
|
49
|
-
spec = event_store.read.limit(count)
|
|
50
|
-
spec = spec.stream(stream_name) unless stream_name.eql?(SERIALIZED_GLOBAL_STREAM_NAME)
|
|
51
|
-
spec = spec.from(position) unless position.equal?(HEAD)
|
|
52
|
-
spec.to_a
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def events_backward(position)
|
|
56
|
-
spec = event_store.read.limit(count).backward
|
|
57
|
-
spec = spec.stream(stream_name) unless stream_name.eql?(SERIALIZED_GLOBAL_STREAM_NAME)
|
|
58
|
-
spec = spec.from(position) unless position.equal?(HEAD)
|
|
59
|
-
spec.to_a
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def next_event?
|
|
63
|
-
return if events.empty?
|
|
64
|
-
events_backward(events.last.event_id).any?
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def prev_event?
|
|
68
|
-
return if events.empty?
|
|
69
|
-
events_forward(events.first.event_id).any?
|
|
70
|
-
end
|
|
34
|
+
end
|
|
71
35
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
36
|
+
def count
|
|
37
|
+
Integer(page["count"] || PAGE_SIZE)
|
|
38
|
+
end
|
|
75
39
|
|
|
76
|
-
|
|
77
|
-
routing.paginated_events_from_stream_url(
|
|
78
|
-
id: stream_name,
|
|
79
|
-
position: event_id,
|
|
80
|
-
direction: :backward,
|
|
81
|
-
count: count,
|
|
82
|
-
)
|
|
83
|
-
end
|
|
40
|
+
private
|
|
84
41
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
42
|
+
def read(reader, position)
|
|
43
|
+
spec = reader.limit(count)
|
|
44
|
+
spec = spec.stream(stream_name) unless stream_name.eql?(SERIALIZED_GLOBAL_STREAM_NAME)
|
|
45
|
+
spec = spec.from(position) if position
|
|
46
|
+
spec.to_a
|
|
47
|
+
end
|
|
88
48
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
49
|
+
def next_event?(found)
|
|
50
|
+
return if found.empty?
|
|
51
|
+
read(event_store.read.backward, found.last.event_id).any?
|
|
52
|
+
end
|
|
92
53
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
54
|
+
def prev_event?(found)
|
|
55
|
+
return if found.empty?
|
|
56
|
+
read(event_store.read, found.first.event_id).any?
|
|
57
|
+
end
|
|
96
58
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
59
|
+
def direction
|
|
60
|
+
case page["direction"]
|
|
61
|
+
when "forward"
|
|
62
|
+
:forward
|
|
63
|
+
else
|
|
64
|
+
:backward
|
|
65
|
+
end
|
|
103
66
|
end
|
|
104
|
-
end
|
|
105
67
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
when "head", nil
|
|
109
|
-
HEAD
|
|
110
|
-
else
|
|
111
|
-
page.fetch("position")
|
|
68
|
+
def position
|
|
69
|
+
page["position"] unless page["position"].nil? || page["position"] == "head"
|
|
112
70
|
end
|
|
113
71
|
end
|
|
114
|
-
end
|
|
115
72
|
end
|
|
116
73
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyEventStore
|
|
4
|
+
module Browser
|
|
5
|
+
class GetEventsFromStreams
|
|
6
|
+
def initialize(event_store:, stream_names:, cursor: nil, sort: nil, count: PAGE_SIZE)
|
|
7
|
+
@event_store = event_store
|
|
8
|
+
@stream_names = stream_names
|
|
9
|
+
@cursor = cursor
|
|
10
|
+
@sort = sort
|
|
11
|
+
@count = count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def events
|
|
15
|
+
@events ||= build_page
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def more?
|
|
19
|
+
events
|
|
20
|
+
@more
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def missing_stream_names
|
|
24
|
+
(stream_names - [SERIALIZED_GLOBAL_STREAM_NAME]).select { |name| chunks.fetch(name).empty? }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def next_cursor
|
|
28
|
+
events.last && time_of(events.last.last).iso8601(TIMESTAMP_PRECISION)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :event_store, :stream_names, :cursor, :sort, :count
|
|
34
|
+
|
|
35
|
+
def build_page
|
|
36
|
+
rows = stream_names.flat_map { |name| chunks.fetch(name).map { |event| [name, event] } }
|
|
37
|
+
@more = full_chunks.any?
|
|
38
|
+
return group(rows) unless @more
|
|
39
|
+
|
|
40
|
+
boundary = full_chunks.map { |name| time_of(chunks.fetch(name).last) }.max
|
|
41
|
+
page_rows = rows.select { |_, event| time_of(event) >= boundary }
|
|
42
|
+
group(page_rows + drained_rows(boundary))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def chunks
|
|
46
|
+
@chunks ||= stream_names.to_h { |name| [name, read_chunk(name)] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def read_chunk(name)
|
|
50
|
+
scope = time_sorted(stream_scope(name)).backward.limit(count)
|
|
51
|
+
scope = scope.older_than(cursor_time) if cursor_time
|
|
52
|
+
scope.to_a
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def full_chunks
|
|
56
|
+
stream_names.select { |name| chunks.fetch(name).size == count }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def drained_rows(boundary)
|
|
60
|
+
full_chunks
|
|
61
|
+
.select { |name| time_of(chunks.fetch(name).last) == boundary }
|
|
62
|
+
.flat_map { |name| drain(name, boundary).map { |event| [name, event] } }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def drain(name, boundary)
|
|
66
|
+
time_sorted(stream_scope(name)).between(boundary..boundary)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stream_scope(name)
|
|
70
|
+
name == SERIALIZED_GLOBAL_STREAM_NAME ? event_store.read : event_store.read.stream(name)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def group(rows)
|
|
74
|
+
rows
|
|
75
|
+
.group_by(&:last)
|
|
76
|
+
.map { |event, list| [list.map(&:first).uniq, event] }
|
|
77
|
+
.sort_by { |_, event| [time_of(event), event.event_id] }
|
|
78
|
+
.reverse
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def time_sorted(scope)
|
|
82
|
+
as_of? ? scope.as_of : scope.as_at
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def time_of(event)
|
|
86
|
+
event.metadata.fetch(as_of? ? :valid_at : :timestamp)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def as_of?
|
|
90
|
+
sort == "as_of"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cursor_time
|
|
94
|
+
@cursor_time ||= cursor && Time.iso8601(cursor)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|