peatio-dao 3.1.3

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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yml +30 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +148 -0
  6. data/.simplecov +17 -0
  7. data/.travis.yml +18 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +242 -0
  10. data/README.md +47 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/peatio +12 -0
  14. data/bin/setup +8 -0
  15. data/lib/peatio/adapter_registry.rb +25 -0
  16. data/lib/peatio/auth/error.rb +18 -0
  17. data/lib/peatio/auth/jwt_authenticator.rb +127 -0
  18. data/lib/peatio/block.rb +29 -0
  19. data/lib/peatio/blockchain/abstract.rb +161 -0
  20. data/lib/peatio/blockchain/error.rb +37 -0
  21. data/lib/peatio/blockchain/registry.rb +16 -0
  22. data/lib/peatio/command/base.rb +11 -0
  23. data/lib/peatio/command/db.rb +20 -0
  24. data/lib/peatio/command/inject.rb +13 -0
  25. data/lib/peatio/command/root.rb +14 -0
  26. data/lib/peatio/command/security.rb +29 -0
  27. data/lib/peatio/command/service.rb +40 -0
  28. data/lib/peatio/error.rb +18 -0
  29. data/lib/peatio/executor.rb +64 -0
  30. data/lib/peatio/injectors/peatio_events.rb +240 -0
  31. data/lib/peatio/logger.rb +39 -0
  32. data/lib/peatio/metrics/server.rb +15 -0
  33. data/lib/peatio/mq/client.rb +51 -0
  34. data/lib/peatio/ranger/connection.rb +117 -0
  35. data/lib/peatio/ranger/events.rb +11 -0
  36. data/lib/peatio/ranger/router.rb +234 -0
  37. data/lib/peatio/ranger/web_socket.rb +68 -0
  38. data/lib/peatio/security/key_generator.rb +26 -0
  39. data/lib/peatio/sql/client.rb +19 -0
  40. data/lib/peatio/sql/schema.rb +72 -0
  41. data/lib/peatio/transaction.rb +137 -0
  42. data/lib/peatio/upstream/base.rb +116 -0
  43. data/lib/peatio/upstream/registry.rb +14 -0
  44. data/lib/peatio/version.rb +3 -0
  45. data/lib/peatio/wallet/abstract.rb +189 -0
  46. data/lib/peatio/wallet/error.rb +37 -0
  47. data/lib/peatio/wallet/registry.rb +16 -0
  48. data/lib/peatio.rb +47 -0
  49. data/peatio-dao.gemspec +53 -0
  50. metadata +455 -0
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Injectors
4
+ class PeatioEvents
5
+ attr_accessor :market, :market_name, :base_unit, :quote_unit, :seller_uid, :buyer_uid, :logger
6
+
7
+ def run!(exchange_name)
8
+ require "time"
9
+ @logger = Peatio::Logger.logger
10
+ @market = "eurusd"
11
+ @market_name = "EUR/USD"
12
+ @base_unit = "eur"
13
+ @quote_unit = "usd"
14
+ @seller_uid = 21
15
+ @buyer_uid = 42
16
+ @messages = create_messages
17
+ @opts = {
18
+ ex_name: exchange_name
19
+ }
20
+ EventMachine.run do
21
+ inject_message()
22
+ end
23
+ end
24
+
25
+ def inject_message()
26
+ if message = @messages.shift
27
+ type, id, event, data = message
28
+ Peatio::Ranger::Events.publish(type, id, event, data, @opts)
29
+ EM.next_tick do
30
+ inject_message()
31
+ end
32
+ else
33
+ Peatio::MQ::Client.disconnect
34
+ EventMachine.stop
35
+ end
36
+ end
37
+
38
+ def create_messages
39
+ [
40
+ public_tickers,
41
+ public_orderbook,
42
+ private_order,
43
+ private_trade_user1,
44
+ private_trade_user2,
45
+ public_trade,
46
+ public_orderbook_increment1,
47
+ public_orderbook_snapshot1,
48
+ public_orderbook_increment2,
49
+ public_orderbook_increment3,
50
+ ]
51
+ end
52
+
53
+ def created_at
54
+ Time.now - 600
55
+ end
56
+
57
+ def updated_at
58
+ Time.now
59
+ end
60
+
61
+ alias completed_at updated_at
62
+ alias canceled_at updated_at
63
+
64
+ def public_orderbook
65
+ [
66
+ "public",
67
+ market,
68
+ "update",
69
+ {
70
+ "asks": [
71
+ ["1020.0", "0.005"],
72
+ ["1026.0", "0.03"]
73
+ ],
74
+ "bids": [
75
+ ["1000.0", "0.25"],
76
+ ["999.0", "0.005"],
77
+ ["994.0", "0.005"],
78
+ ["1.0", "11.0"]
79
+ ]
80
+ }
81
+ ]
82
+ end
83
+
84
+ def public_orderbook_snapshot1
85
+ [
86
+ "public",
87
+ market,
88
+ "ob-snap",
89
+ {
90
+ "asks": [
91
+ ["1020.0", "0.005"],
92
+ ["1026.0", "0.03"]
93
+ ],
94
+ "bids": [
95
+ ["1000.0", "0.25"],
96
+ ["999.0", "0.005"],
97
+ ["994.0", "0.005"],
98
+ ["1.0", "11.0"]
99
+ ]
100
+ }
101
+ ]
102
+ end
103
+
104
+ def public_orderbook_increment1
105
+ [
106
+ "public",
107
+ market,
108
+ "ob-inc",
109
+ {
110
+ "asks": [
111
+ ["1020.0", "0.015"],
112
+ ],
113
+ }
114
+ ]
115
+ end
116
+
117
+ def public_orderbook_increment2
118
+ [
119
+ "public",
120
+ market,
121
+ "ob-inc",
122
+ {
123
+ "bids": [
124
+ ["1000.0", "0"],
125
+ ],
126
+ }
127
+ ]
128
+ end
129
+
130
+ def public_orderbook_increment3
131
+ [
132
+ "public",
133
+ market,
134
+ "ob-inc",
135
+ {
136
+ "bids": [
137
+ ["999.0", "0.001"],
138
+ ],
139
+ }
140
+ ]
141
+ end
142
+
143
+ def public_tickers
144
+ [
145
+ "public",
146
+ "global",
147
+ "tickers",
148
+ {
149
+ market => {
150
+ "name": market_name,
151
+ "base_unit": base_unit,
152
+ "quote_unit": quote_unit,
153
+ "low": "1000.0",
154
+ "high": "10000.0",
155
+ "last": "1000.0",
156
+ "open": 1000.0,
157
+ "volume": "0.0",
158
+ "sell": "1020.0",
159
+ "buy": "1000.0",
160
+ "at": Time.now.to_i
161
+ }
162
+ }
163
+ ]
164
+ end
165
+
166
+ def private_order
167
+ [
168
+ "private",
169
+ "IDABC0000001",
170
+ "order",
171
+ {
172
+ "id": 22,
173
+ "at": created_at.to_i,
174
+ "market": market,
175
+ "kind": "bid",
176
+ "price": "1026.0",
177
+ "state": "wait",
178
+ "volume": "0.001",
179
+ "origin_volume": "0.001"
180
+ }
181
+ ]
182
+ end
183
+
184
+ def private_trade_user1
185
+ [
186
+ "private",
187
+ "IDABC0000001",
188
+ "trade",
189
+ {
190
+ "id": 7,
191
+ "kind": "ask",
192
+ "at": created_at.to_i,
193
+ "price": "1020.0",
194
+ "volume": "0.001",
195
+ "ask_id": 15,
196
+ "bid_id": 22,
197
+ "market": market
198
+ }
199
+ ]
200
+ end
201
+
202
+ def private_trade_user2
203
+ [
204
+ "private",
205
+ "IDABC0000002",
206
+ "trade",
207
+ {
208
+ "id": 7,
209
+ "kind": "bid",
210
+ "at": created_at.to_i,
211
+ "price": "1020.0",
212
+ "volume": "0.001",
213
+ "ask_id": 15,
214
+ "bid_id": 22,
215
+ "market": market
216
+ }
217
+ ]
218
+ end
219
+
220
+ def public_trade
221
+ [
222
+ "public",
223
+ market,
224
+ "trades",
225
+ {
226
+ "trades": [
227
+ {
228
+ "tid": 7,
229
+ "taker_type": "buy",
230
+ "date": created_at.to_i,
231
+ "price": "1020.0",
232
+ "amount":
233
+ "0.001"
234
+ }
235
+ ]
236
+ }
237
+ ]
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio
4
+ class Logger
5
+ class << self
6
+ def logger
7
+ @logger ||= ::Logger.new(STDERR, level: level)
8
+ end
9
+
10
+ def level
11
+ (ENV["LOG_LEVEL"] || "info").downcase.to_sym
12
+ end
13
+
14
+ def debug(progname=nil, &block)
15
+ logger.debug(progname, &block)
16
+ end
17
+
18
+ def info(progname=nil, &block)
19
+ logger.info(progname, &block)
20
+ end
21
+
22
+ def warn(progname=nil, &block)
23
+ logger.warn(progname, &block)
24
+ end
25
+
26
+ def error(progname=nil, &block)
27
+ logger.error(progname, &block)
28
+ end
29
+
30
+ def fatal(progname=nil, &block)
31
+ logger.fatal(progname, &block)
32
+ end
33
+
34
+ def unknown(progname=nil, &block)
35
+ logger.unknown(progname, &block)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Metrics
4
+ class Server
5
+ def self.app(registry)
6
+ Rack::Builder.new do |builder|
7
+ builder.use Rack::CommonLogger
8
+ builder.use Rack::ShowExceptions
9
+ builder.use Rack::Deflater
10
+ builder.use Prometheus::Middleware::Exporter, registry: registry
11
+ builder.run ->(_) { [404, {"Content-Type" => "text/html"}, ["Not found\n"]] }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::MQ
4
+ class Client
5
+ class << self
6
+ attr_accessor :connection
7
+
8
+ def connect!
9
+ options = {
10
+ host: ENV["RABBITMQ_HOST"] || "0.0.0.0",
11
+ port: ENV["RABBITMQ_PORT"] || "5672",
12
+ username: ENV["RABBITMQ_USER"],
13
+ password: ENV["RABBITMQ_PASSWORD"],
14
+ }
15
+ @connection = Bunny.new(options)
16
+ @connection.start
17
+ end
18
+
19
+ def disconnect
20
+ @connection.close
21
+ end
22
+ end
23
+
24
+ def initialize
25
+ Client.connect! unless Peatio::MQ::Client.connection
26
+ @channel = Client.connection.create_channel
27
+ @exchanges = {}
28
+ end
29
+
30
+ def exchange(name, type="topic")
31
+ @exchanges[name] ||= @channel.exchange(name, type: type)
32
+ end
33
+
34
+ def publish(ex_name, type, id, event, payload)
35
+ routing_key = [type, id, event].join(".")
36
+ serialized_data = JSON.dump(payload)
37
+ exchange(ex_name).publish(serialized_data, routing_key: routing_key)
38
+ Peatio::Logger.debug { "published event to #{routing_key} " }
39
+ end
40
+
41
+ def subscribe(ex_name, &callback)
42
+ suffix = "#{Socket.gethostname.split(/-/).last}#{Random.rand(10_000)}"
43
+ queue_name = "ranger.#{suffix}"
44
+
45
+ @channel
46
+ .queue(queue_name, durable: false, auto_delete: true)
47
+ .bind(exchange(ex_name), routing_key: "#").subscribe(&callback)
48
+ Peatio::Logger.info "Subscribed to exchange #{ex_name}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,117 @@
1
+ module Peatio::Ranger
2
+ class Connection
3
+ attr_reader :socket, :user, :authorized, :streams, :logger, :id
4
+
5
+ def initialize(router, socket, logger)
6
+ @id = SecureRandom.hex(10)
7
+ @router = router
8
+ @socket = socket
9
+ @logger = logger
10
+ @user = nil
11
+ @authorized = false
12
+ @streams = {}
13
+ end
14
+
15
+ def inspect
16
+ if authorized
17
+ "<Connection id=#{id} user=#{user}>"
18
+ else
19
+ "<Connection id=#{id}>"
20
+ end
21
+ end
22
+
23
+ def send_raw(payload)
24
+ if user
25
+ logger.debug { "sending to user #{user.inspect} payload: #{payload}" }
26
+ else
27
+ logger.debug { "sending to anonymous payload: #{payload}" }
28
+ end
29
+ @socket.send(payload)
30
+ end
31
+
32
+ def send(method, data)
33
+ payload = JSON.dump(method => data)
34
+ send_raw(payload)
35
+ end
36
+
37
+ def authenticate(authenticator, jwt)
38
+ payload = {}
39
+ authorized = false
40
+ begin
41
+ payload = authenticator.authenticate!(jwt)
42
+ authorized = true
43
+ rescue Peatio::Auth::Error => e
44
+ logger.warn e.message
45
+ end
46
+ [authorized, payload]
47
+ end
48
+
49
+ def subscribe(subscribed_streams)
50
+ raise "Streams must be an array of strings" unless subscribed_streams.is_a?(Array)
51
+
52
+ subscribed_streams.each do |stream|
53
+ stream = stream.to_s
54
+ next if stream.empty?
55
+
56
+ unless @streams[stream]
57
+ @streams[stream] = true
58
+ @router.on_subscribe(self, stream)
59
+ end
60
+ end
61
+ send(:success, message: "subscribed", streams: streams.keys)
62
+ end
63
+
64
+ def unsubscribe(unsubscribed_streams)
65
+ raise "Streams must be an array of strings" unless unsubscribed_streams.is_a?(Array)
66
+
67
+ unsubscribed_streams.each do |stream|
68
+ stream = stream.to_s
69
+ next if stream.empty?
70
+
71
+ if @streams[stream]
72
+ @streams.delete(stream)
73
+ @router.on_unsubscribe(self, stream)
74
+ end
75
+ end
76
+ send(:success, message: "unsubscribed", streams: streams.keys)
77
+ end
78
+
79
+ def handle(msg)
80
+ return if msg.to_s.empty?
81
+
82
+ if msg =~ /^ping/
83
+ send_raw("pong")
84
+ return
85
+ end
86
+
87
+ data = JSON.parse(msg)
88
+ case data["event"]
89
+ when "subscribe"
90
+ subscribe(data["streams"])
91
+ when "unsubscribe"
92
+ unsubscribe(data["streams"])
93
+ end
94
+ rescue JSON::ParserError => e
95
+ logger.debug { "#{e}, msg: `#{msg}`" }
96
+ end
97
+
98
+ def handshake(authenticator, hs)
99
+ query = URI.decode_www_form(hs.query_string)
100
+ subscribe(query.map {|item| item.last if item.first == "stream" })
101
+ logger.debug "WebSocket connection openned"
102
+ headers = hs.headers_downcased
103
+ return unless headers.key?("authorization")
104
+
105
+ authorized, payload = authenticate(authenticator, headers["authorization"])
106
+
107
+ if !authorized
108
+ logger.debug "Authentication failed for UID:#{payload[:uid]}"
109
+ raise EM::WebSocket::HandshakeError, "Authorization failed"
110
+ else
111
+ @user = payload[:uid]
112
+ @authorized = true
113
+ logger.debug "User #{@user} authenticated #{@streams}"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Ranger
4
+ class Events
5
+ def self.publish(type, id, event, payload, opts={})
6
+ ex_name = opts[:ex_name] || "peatio.events.ranger"
7
+ @client ||= Peatio::MQ::Client.new
8
+ @client.publish(ex_name, type, id, event, payload)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Ranger
4
+ class Router
5
+ attr_reader :connections
6
+ attr_reader :connections_by_userid
7
+ attr_reader :streams_sockets
8
+ attr_reader :logger
9
+
10
+ class ConnectionArray < Array
11
+ def delete(connection)
12
+ delete_if do |c|
13
+ c.id == connection.id
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(prometheus=nil)
19
+ @connections = {}
20
+ @connections_by_userid = {}
21
+ @streams_sockets = {}
22
+ @logger = Peatio::Logger.logger
23
+ @stores = {}
24
+ init_metrics(prometheus)
25
+ end
26
+
27
+ def init_metrics(prometheus)
28
+ return unless prometheus
29
+
30
+ @prometheus = prometheus
31
+ @metric_connections_total = @prometheus.counter(
32
+ :ranger_connections_total,
33
+ docstring: "Total number of connections to ranger from the start",
34
+ labels: [:auth]
35
+ )
36
+ @metric_connections_current = @prometheus.gauge(
37
+ :ranger_connections_current,
38
+ docstring: "Current number of connections to ranger",
39
+ labels: [:auth]
40
+ )
41
+ @metric_subscriptions_current = @prometheus.gauge(
42
+ :ranger_subscriptions_current,
43
+ docstring: "Current number of streams subscriptions to ranger",
44
+ labels: [:stream]
45
+ )
46
+ end
47
+
48
+ def snapshot?(stream)
49
+ stream.end_with?("-snap")
50
+ end
51
+
52
+ def increment?(stream)
53
+ stream.end_with?("-inc")
54
+ end
55
+
56
+ def storekey(stream)
57
+ stream.gsub(/-(snap|inc)$/, "")
58
+ end
59
+
60
+ def stats
61
+ [
62
+ "==== Metrics ====",
63
+ "ranger_connections_total{auth=\"public\"}: %d" % [@metric_connections_total.get(labels: {auth: "public"})],
64
+ "ranger_connections_total{auth=\"private\"}: %d" % [@metric_connections_total.get(labels: {auth: "private"})],
65
+ "ranger_connections_current{auth=\"public\"}: %d" % [@metric_connections_current.get(labels: {auth: "public"})],
66
+ "ranger_connections_current{auth=\"private\"}: %d" % [@metric_connections_current.get(labels: {auth: "private"})],
67
+ "ranger_subscriptions_current: %d" % [compute_streams_subscriptions()],
68
+ "ranger_streams_kinds: %d" % [compute_streams_kinds()],
69
+ ].join("\n")
70
+ end
71
+
72
+ def debug
73
+ [
74
+ "==== Debug ====",
75
+ "connections: %s" % [@connections.inspect],
76
+ "connections_by_userid: %s" % [@connections_by_userid],
77
+ "streams_sockets: %s" % [@streams_sockets],
78
+ ].join("\n")
79
+ end
80
+
81
+ def compute_connections_all
82
+ @connections.size
83
+ end
84
+
85
+ def compute_connections_private
86
+ @connections_by_userid.each_value.map(&:size).reduce(0, :+)
87
+ end
88
+
89
+ def compute_stream_subscriptions(stream)
90
+ @streams_sockets[stream]&.size || 0
91
+ end
92
+
93
+ def compute_streams_subscriptions
94
+ @streams_sockets.each_value.map(&:size).reduce(0, :+)
95
+ end
96
+
97
+ def compute_streams_kinds
98
+ @streams_sockets.size
99
+ end
100
+
101
+ def sanity_check_metrics_connections
102
+ return unless @metric_connections_current
103
+
104
+ connections_current_all = @metric_connections_current.values.values.reduce(0, :+)
105
+ return if connections_current_all == compute_connections_all()
106
+
107
+ logger.warn "slip detected in metric_connections_current, recalculating"
108
+ connections_current_private = compute_connections_private()
109
+ @metric_connections_current.set(connections_current_private, labels: {auth: "private"})
110
+ @metric_connections_current.set(compute_connections_all() - connections_current_private, labels: {auth: "public"})
111
+ end
112
+
113
+ def on_connection_open(connection)
114
+ @connections[connection.id] = connection
115
+ unless connection.authorized
116
+ @metric_connections_current&.increment(labels: {auth: "public"})
117
+ @metric_connections_total&.increment(labels: {auth: "public"})
118
+ return
119
+ end
120
+ @metric_connections_current&.increment(labels: {auth: "private"})
121
+ @metric_connections_total&.increment(labels: {auth: "private"})
122
+
123
+ @connections_by_userid[connection.user] ||= ConnectionArray.new
124
+ @connections_by_userid[connection.user] << connection
125
+ end
126
+
127
+ def on_connection_close(connection)
128
+ @connections.delete(connection.id)
129
+ connection.streams.keys.each do |stream|
130
+ on_unsubscribe(connection, stream)
131
+ end
132
+
133
+ unless connection.authorized
134
+ @metric_connections_current&.decrement(labels: {auth: "public"})
135
+ sanity_check_metrics_connections
136
+ return
137
+ end
138
+ @metric_connections_current&.decrement(labels: {auth: "private"})
139
+
140
+ @connections_by_userid[connection.user].delete(connection)
141
+ @connections_by_userid.delete(connection.user) \
142
+ if @connections_by_userid[connection.user].empty?
143
+ sanity_check_metrics_connections
144
+ end
145
+
146
+ def on_subscribe(connection, stream)
147
+ @streams_sockets[stream] ||= ConnectionArray.new
148
+ @streams_sockets[stream] << connection
149
+ send_snapshot_and_increments(connection, storekey(stream)) if increment?(stream)
150
+ @metric_subscriptions_current&.set(compute_stream_subscriptions(stream), labels: {stream: stream})
151
+ end
152
+
153
+ def send_snapshot_and_increments(connection, key)
154
+ return unless @stores[key]
155
+ return unless @stores[key][:snapshot]
156
+
157
+ connection.send_raw(@stores[key][:snapshot])
158
+ @stores[key][:increments]&.each {|inc| connection.send_raw(inc) }
159
+ end
160
+
161
+ def on_unsubscribe(connection, stream)
162
+ return unless @streams_sockets[stream]
163
+
164
+ @streams_sockets[stream].delete(connection)
165
+ @streams_sockets.delete(stream) if @streams_sockets[stream].empty?
166
+ @metric_subscriptions_current&.set(compute_stream_subscriptions(stream), labels: {stream: stream})
167
+ end
168
+
169
+ def send_private_message(user_id, event, payload_decoded)
170
+ Array(@connections_by_userid[user_id]).each do |connection|
171
+ connection.send(event, payload_decoded) if connection.streams.include?(event)
172
+ end
173
+ end
174
+
175
+ def send_public_message(stream, raw_message)
176
+ Array(@streams_sockets[stream]).each do |connection|
177
+ connection.send_raw(raw_message)
178
+ end
179
+ end
180
+
181
+ #
182
+ # routing key format: type.id.event
183
+ # * `type` can be *public* or *private*
184
+ # * `id` can be user id or market id
185
+ # * `event` is the event identifier, ex: order_completed, trade, ...
186
+ #
187
+ def on_message(delivery_info, _metadata, payload)
188
+ routing_key = delivery_info.routing_key
189
+ if routing_key.count(".") != 2
190
+ logger.error { "invalid routing key from amqp: #{routing_key}" }
191
+ return
192
+ end
193
+
194
+ type, id, event = routing_key.split(".")
195
+ payload_decoded = JSON.parse(payload)
196
+
197
+ if type == "private"
198
+ send_private_message(id, event, payload_decoded)
199
+ return
200
+ end
201
+
202
+ stream = [id, event].join(".")
203
+ message = JSON.dump(stream => payload_decoded)
204
+
205
+ if snapshot?(event)
206
+ key = storekey(stream)
207
+
208
+ unless @stores[key]
209
+ # Send the snapshot to subscribers of -inc stream if there were no snapshot before
210
+ send_public_message("#{key}-inc", message)
211
+ end
212
+
213
+ @stores[key] = {
214
+ snapshot: message,
215
+ increments: [],
216
+ }
217
+ return
218
+ end
219
+
220
+ if increment?(event)
221
+ key = storekey(stream)
222
+
223
+ unless @stores[key]
224
+ logger.warn { "Discard increment received before snapshot for store:#{key}" }
225
+ return
226
+ end
227
+
228
+ @stores[key][:increments] << message
229
+ end
230
+
231
+ send_public_message(stream, message)
232
+ end
233
+ end
234
+ end