forked from eventide-examples/account-basics
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathservice.rb
218 lines (170 loc) · 5.16 KB
/
service.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
require_relative 'gems/bundler/setup'
require 'eventide/postgres'
require 'consumer/postgres'
require 'component_host'
# Deposit command message
# Send to the account component to effect a deposit
class Deposit
include Messaging::Message
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
end
# Deposited event message
# Event is written by the handler when a deposit is successfully processed
class Deposited
include Messaging::Message
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
attribute :processed_time, String
end
# Withdraw command message
# Send to the account component to effect a withdrawal
class Withdraw
include Messaging::Message
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
end
# Withdrawn event message
# Event is written by the handler when a withdrawal is successfully processed
class Withdrawn
include Messaging::Message
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
attribute :processed_time, String
end
# WithdrawalRejected event message
# Event is written by the handler when a withdrawal cannot be successfully
# processed, as when there are insufficient funds
class WithdrawalRejected
include Messaging::Message
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
end
# Account entity
# The account component's model object
class Account
include Schema::DataStructure
attribute :id, String
attribute :balance, Numeric, default: 0
def deposit(amount)
self.balance += amount
end
def withdraw(amount)
self.balance -= amount
end
def sufficient_funds?(amount)
balance >= amount
end
end
# Account transformation to and from JSON for entity snapshotting
# For more info on transformation, see:
# https://github.com/eventide-project/transform
class Account
module Transform
# When reading: Convert hash to Account
def self.instance(raw_data)
Account.build(raw_data)
end
# When writing: Convert Account to hash
def self.raw_data(instance)
instance.to_h
end
end
end
# Account entity projection
# Applies account events to an account entity
class Projection
include EntityProjection
entity_name :account
apply Deposited do |deposited|
amount = deposited.amount
account.deposit(amount)
# It's impossible to know which event will be received first
# so assign the account ID in every event's projection
account.id = deposited.account_id
end
apply Withdrawn do |withdrawn|
amount = withdrawn.amount
account.withdraw(amount)
# It's impossible to know which event will be received first
# so assign the account ID in every event's projection
account.id = withdrawn.account_id
end
end
# Account entity store
# Projects an account entity and keeps a cache of the result
class Store
include EntityStore
category :account
entity Account
projection Projection
reader MessageStore::Postgres::Read
snapshot EntitySnapshot::Postgres, interval: 5
end
# Account command handler with withdrawal implementation
# Business logic for processing a withdrawal
class Handler
include Messaging::Handle
include Messaging::StreamName
dependency :write, Messaging::Postgres::Write
dependency :clock, Clock::UTC
dependency :store, Store
def configure
Messaging::Postgres::Write.configure(self)
Clock::UTC.configure(self)
Store.configure(self)
end
category :account
handle Deposit do |deposit|
account_id = deposit.account_id
time = clock.iso8601
deposited = Deposited.follow(deposit)
deposited.processed_time = time
stream_name = stream_name(account_id)
write.(deposited, stream_name)
end
handle Withdraw do |withdraw|
account_id = withdraw.account_id
account = store.fetch(account_id)
time = clock.iso8601
stream_name = stream_name(account_id)
unless account.sufficient_funds?(withdraw.amount)
withdrawal_rejected = WithdrawalRejected.follow(withdraw)
withdrawal_rejected.time = time
write.(withdrawal_rejected, stream_name)
return
end
withdrawn = Withdrawn.follow(withdraw)
withdrawn.processed_time = time
write.(withdrawn, stream_name)
end
end
# The consumer dispatches in-bound messages to handlers
# Consumers have an internal reader that reads messages from a single stream
# Consumers can have many handlers
class AccountConsumer
include Consumer::Postgres
handler Handler
end
# The "Component" module maps consumers to their streams
# Until this point, handlers have no knowledge of which streams they process
# Starting the consumers starts the stream readers and gets messages
# flowing into the consumer's handlers
module Component
def self.call
account_command_stream_name = 'account:command'
AccountConsumer.start(account_command_stream_name)
end
end
# ComponentHost is the runnable part of the service
# Register the Start module with the component host, then start the
# component and messages sent to its streams are dispatched to the handlers
component_name = 'account-service'
ComponentHost.start(component_name) do |host|
host.register(Component)
end