hyper-store 1.0.alpha1.8 → 1.0.0.lap28
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -4
- data/.rubocop.yml +107 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/DOCS.md +312 -0
- data/Gemfile +1 -4
- data/LICENSE.txt +21 -0
- data/README.md +80 -0
- data/Rakefile +2 -0
- data/dciy.toml +3 -0
- data/hyper-store.gemspec +18 -20
- data/lib/hyper-store.rb +16 -19
- data/lib/hyper-store/class_methods.rb +32 -0
- data/lib/hyper-store/dispatch_receiver.rb +38 -0
- data/lib/hyper-store/instance_methods.rb +40 -0
- data/lib/hyper-store/mutator_wrapper.rb +71 -0
- data/lib/hyper-store/state_wrapper.rb +107 -0
- data/lib/hyper-store/state_wrapper/argument_validator.rb +91 -0
- data/lib/hyper-store/version.rb +3 -0
- data/lib/hyperloop/application/boot.rb +34 -0
- data/lib/hyperloop/store.rb +12 -0
- data/lib/hyperloop/store/mixin.rb +21 -0
- data/lib/react/observable.rb +29 -0
- data/lib/react/state.rb +143 -0
- metadata +64 -116
- data/lib/hyperstack/internal/store/class_methods.rb +0 -36
- data/lib/hyperstack/internal/store/dispatch_receiver.rb +0 -41
- data/lib/hyperstack/internal/store/instance_methods.rb +0 -45
- data/lib/hyperstack/internal/store/mutator_wrapper.rb +0 -84
- data/lib/hyperstack/internal/store/observable.rb +0 -33
- data/lib/hyperstack/internal/store/state_wrapper.rb +0 -116
- data/lib/hyperstack/internal/store/state_wrapper/argument_validator.rb +0 -93
- data/lib/hyperstack/legacy/store.rb +0 -23
- data/lib/hyperstack/legacy/store/version.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61561ae2ef17af35227e0a5c5227512567e0f3006be750477727e71fd3008e2d
|
4
|
+
data.tar.gz: ac9c2a5d2f4f7193ae73b78fd9f30a00ca5b73ac11c85f765b78792473983872
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7003f127e4551440f30587ac10207f54fd5d06a39162f327e5963cc9bcd79c9aeafa200e0040c531a875040fd4fc5ca42fc9968e18c6ee331cbdc871046c466f
|
7
|
+
data.tar.gz: 6aa28c8d48442ecc0f24264a6ea966c09e8b81b5e2b5ff57e03c724b30691b765c51f9df1a73fe84268a252df811e2dffe8ce25adb69169fe687b486e57465e0
|
data/.gitignore
CHANGED
@@ -11,6 +11,7 @@ capybara-*.html
|
|
11
11
|
**.orig
|
12
12
|
rerun.txt
|
13
13
|
pickle-email-*.html
|
14
|
+
Gemfile.lock
|
14
15
|
|
15
16
|
# TODO Comment out these rules if you are OK with secrets being uploaded to the repo
|
16
17
|
config/initializers/secret_token.rb
|
@@ -50,7 +51,3 @@ bower.json
|
|
50
51
|
|
51
52
|
# ignore IDE files
|
52
53
|
.idea
|
53
|
-
|
54
|
-
# ignore Gemfile.locks https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
|
55
|
-
/spec/test_app/Gemfile.lock
|
56
|
-
/Gemfile.lock
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
Style/BlockDelimiters:
|
2
|
+
EnforcedStyle: line_count_based
|
3
|
+
SupportedStyles:
|
4
|
+
# The `line_count_based` style enforces braces around single line blocks and
|
5
|
+
# do..end around multi-line blocks.
|
6
|
+
- line_count_based
|
7
|
+
# The `semantic` style enforces braces around functional blocks, where the
|
8
|
+
# primary purpose of the block is to return a value and do..end for
|
9
|
+
# procedural blocks, where the primary purpose of the block is its
|
10
|
+
# side-effects.
|
11
|
+
#
|
12
|
+
# This looks at the usage of a block's method to determine its type (e.g. is
|
13
|
+
# the result of a `map` assigned to a variable or passed to another
|
14
|
+
# method) but exceptions are permitted in the `ProceduralMethods`,
|
15
|
+
# `FunctionalMethods` and `IgnoredMethods` sections below.
|
16
|
+
- semantic
|
17
|
+
# The `braces_for_chaining` style enforces braces around single line blocks
|
18
|
+
# and do..end around multi-line blocks, except for multi-line blocks whose
|
19
|
+
# return value is being chained with another method (in which case braces
|
20
|
+
# are enforced).
|
21
|
+
- braces_for_chaining
|
22
|
+
ProceduralMethods:
|
23
|
+
# Methods that are known to be procedural in nature but look functional from
|
24
|
+
# their usage, e.g.
|
25
|
+
#
|
26
|
+
# time = Benchmark.realtime do
|
27
|
+
# foo.bar
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Here, the return value of the block is discarded but the return value of
|
31
|
+
# `Benchmark.realtime` is used.
|
32
|
+
- benchmark
|
33
|
+
- bm
|
34
|
+
- bmbm
|
35
|
+
- create
|
36
|
+
- each_with_object
|
37
|
+
- measure
|
38
|
+
- new
|
39
|
+
- realtime
|
40
|
+
- tap
|
41
|
+
- with_object
|
42
|
+
FunctionalMethods:
|
43
|
+
# Methods that are known to be functional in nature but look procedural from
|
44
|
+
# their usage, e.g.
|
45
|
+
#
|
46
|
+
# let(:foo) { Foo.new }
|
47
|
+
#
|
48
|
+
# Here, the return value of `Foo.new` is used to define a `foo` helper but
|
49
|
+
# doesn't appear to be used from the return value of `let`.
|
50
|
+
- let
|
51
|
+
- let!
|
52
|
+
- subject
|
53
|
+
- watch
|
54
|
+
IgnoredMethods:
|
55
|
+
# Methods that can be either procedural or functional and cannot be
|
56
|
+
# categorised from their usage alone, e.g.
|
57
|
+
#
|
58
|
+
# foo = lambda do |x|
|
59
|
+
# puts "Hello, #{x}"
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# foo = lambda do |x|
|
63
|
+
# x * 100
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# Here, it is impossible to tell from the return value of `lambda` whether
|
67
|
+
# the inner block's return value is significant.
|
68
|
+
- lambda
|
69
|
+
- proc
|
70
|
+
- it
|
71
|
+
|
72
|
+
Style/Documentation:
|
73
|
+
Description: 'Document classes and non-namespace modules.'
|
74
|
+
Enabled: false
|
75
|
+
Exclude:
|
76
|
+
- 'spec/**/*'
|
77
|
+
- 'test/**/*'
|
78
|
+
|
79
|
+
# Multi-line method chaining should be done with trailing dots.
|
80
|
+
Style/DotPosition:
|
81
|
+
EnforcedStyle: leading
|
82
|
+
SupportedStyles:
|
83
|
+
- leading
|
84
|
+
- trailing
|
85
|
+
|
86
|
+
Style/FrozenStringLiteralComment:
|
87
|
+
Description: >-
|
88
|
+
Add the frozen_string_literal comment to the top of files
|
89
|
+
to help transition from Ruby 2.3.0 to Ruby 3.0.
|
90
|
+
Enabled: false
|
91
|
+
|
92
|
+
Style/MultilineBlockChain:
|
93
|
+
Description: 'Avoid multi-line chains of blocks.'
|
94
|
+
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
|
95
|
+
Enabled: false
|
96
|
+
|
97
|
+
Style/MutableConstant:
|
98
|
+
Description: 'Do not assign mutable objects to constants.'
|
99
|
+
Enabled: false
|
100
|
+
|
101
|
+
Metrics/AbcSize:
|
102
|
+
# The ABC size is a calculated magnitude, so this number can be a Fixnum or
|
103
|
+
# a Float.
|
104
|
+
Max: 20
|
105
|
+
|
106
|
+
Metrics/LineLength:
|
107
|
+
Max: 100
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at mitch@catprint.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/DOCS.md
ADDED
@@ -0,0 +1,312 @@
|
|
1
|
+
# Hyperloop Stores
|
2
|
+
|
3
|
+
Hyperloop **Stores** are implemented in the **HyperStore Gem**.
|
4
|
+
|
5
|
+
Stores are where the state of your Application lives. Anything but a completely static web page will have dynamic states that change because of user inputs, the passage of time, or other external events.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class UserStore < Hyperloop::Store
|
9
|
+
state :current, scope: :class, reader: true
|
10
|
+
|
11
|
+
def self.set_current! user
|
12
|
+
mutate.current user
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# to access the store
|
17
|
+
UserStore.set_current! user
|
18
|
+
UserStore.current_user
|
19
|
+
```
|
20
|
+
|
21
|
+
**Stores are Ruby classes that keep the dynamic parts of the state in special state variables**
|
22
|
+
|
23
|
+
+ `Hyperloop::Store::Mixin` can be mixed in to any class to turn it into a Flux Store.
|
24
|
+
+ You can also create Stores by subclassing `Hyperloop::Store`.
|
25
|
+
+ Stores are built out of *reactive state variables*.
|
26
|
+
+ Components that *read* a Store's state will **automatically** update when the state changes.
|
27
|
+
+ All of your **shared** reactive state should be Stores - *The Store is the Truth*!
|
28
|
+
+ Stores can *receive* **dispatches** from *Operations*
|
29
|
+
|
30
|
+
## Reading and Mutating States
|
31
|
+
|
32
|
+
A Store will have one or more *Reactive State Variables* or *State* for short. States are read using the `state` method, and are changed using the `mutate` method.
|
33
|
+
|
34
|
+
`state.items` reads the current value of the state named `items`. Hyperloop tracks all reads of state, and mutating those states will trigger a re-render of any Components depending on the current value.
|
35
|
+
|
36
|
+
`mutate.items` returns the current value of the state named `items`, but also tells Hyperloop that the value is changing, and that any Components depending on the current value will have to be re-rendered.
|
37
|
+
|
38
|
+
The one thing you must remember to do is use `mutate` if you intend to update the internal value of a state. For example if the state contains a hash, and you are updating the Hash's internal value you would use `mutate` otherwise the change will go unrecorded.
|
39
|
+
|
40
|
+
#### Initializing States
|
41
|
+
|
42
|
+
To assign a new value to a state use the `mutate` method and pass a parameter to the state:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
mutate.items(Hash.new { |h, k| h[k] = 0 })
|
46
|
+
```
|
47
|
+
|
48
|
+
#### Reading States
|
49
|
+
|
50
|
+
To read the current value of a state use the `state` method:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
state.items # returns current value of items
|
54
|
+
```
|
55
|
+
|
56
|
+
Typically a store will have quite a few reader (aka getter) methods that hide the details of the state, allowing the Store's implementation to change, without effecting the interface.
|
57
|
+
|
58
|
+
#### Mutating States
|
59
|
+
|
60
|
+
Often states hold data structures like arrays, hashes, sets, or other Ruby classes, which may be *mutated*. For example when you push a new value onto an array you will mutate it. The *value* of the array does not change, but its *contents* does. If you are accessing a state with the intent to change its content then use the `mutate` method:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
mutate.items[item] = value
|
64
|
+
```
|
65
|
+
|
66
|
+
### Instances and Classes
|
67
|
+
|
68
|
+
Stores are often singleton classes. In an application there is one 'cart' for example.
|
69
|
+
|
70
|
+
However sometimes you will want to create a class where each instance is a Store. This is straight forward because if a state is read or mutated in an instance method, then you will be referring to that instance's copy of the state.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# Each UserStream provides a stream of unique user profiles.
|
74
|
+
# Each instance has a single HyperStore state variable called user
|
75
|
+
# user will contain a single hash representing the user profile.
|
76
|
+
class UserStream < Hyperloop::Store
|
77
|
+
|
78
|
+
# get another user
|
79
|
+
|
80
|
+
def get_another!
|
81
|
+
mutate.user UserStream._select_random_user
|
82
|
+
end
|
83
|
+
|
84
|
+
# extract various attributes from the user hash
|
85
|
+
|
86
|
+
def user_name
|
87
|
+
state.user[:login]
|
88
|
+
end
|
89
|
+
|
90
|
+
def user_url
|
91
|
+
state.user[:html_url]
|
92
|
+
end
|
93
|
+
|
94
|
+
def avatar
|
95
|
+
state.user[:avatar_url]
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize
|
99
|
+
get_another!
|
100
|
+
end
|
101
|
+
|
102
|
+
def self._select_random_user
|
103
|
+
# _select_random_user provides a stream of unique user profiles.
|
104
|
+
# It will either return a user profile hash, or a promise of one
|
105
|
+
# to come.
|
106
|
+
|
107
|
+
# The cache of users to choose from does not have to be an state
|
108
|
+
# variable, so we use plain instance variables.
|
109
|
+
return @users.delete_at(rand(@users.length)) unless @users.blank?
|
110
|
+
# execute the GetMoreUsers Operation to grab another batch of users
|
111
|
+
# if we are not already waiting on a promise
|
112
|
+
@promise = GetMoreUsers.then do |response|
|
113
|
+
@users = response.json
|
114
|
+
end if @promise.nil? || @promise.resolved?
|
115
|
+
# wait for the promise to resolve then try again
|
116
|
+
@promise.then { _select_random_user }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class GetMoreUsers < HyperOperation
|
121
|
+
def execute
|
122
|
+
HTTP.get("https://api.github.com/users?since=#{rand(500)}")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
Stores that have multiple instances will typically have instance methods that directly mutate the store. We recommend you end these methods with an exclamation (!) to make it clear you are exposing a mutator.
|
127
|
+
|
128
|
+
### States and Promises
|
129
|
+
|
130
|
+
The above example is greatly simplified because if a promise is assigned to a state it will not mutate *until the promise resolves*. Combining this with instance Stores gives a powerful way to encapsulate system behavior.
|
131
|
+
|
132
|
+
### Explicitly Declaring States
|
133
|
+
|
134
|
+
States like instance variables are created when they are first referenced.
|
135
|
+
|
136
|
+
As a convenience you may also explicitly declare states. This reduces code noise, and improves readability.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class Cart < Hyperloop::Store
|
140
|
+
state items: Hash.new { |h, k| h[k] = 0 }, scope: :class, reader: true
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
This *declares* the `items` state as a class state variable, will initialize it with the hash on `Hyperloop::Boot`, and provides a reader method.
|
145
|
+
That is 6 lines of code for the price of 1, plus now the intention of `items` is clearly defined.
|
146
|
+
|
147
|
+
The `state` declaration has the following flavors, depending on how the state is to be initialized:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
state :items, ... other options ... # items will be initialized to nil
|
151
|
+
state items: [1, 2, 3], ... other options ... # items will be initialized to the array [1, 2, 3]
|
152
|
+
state :items, ... other options ... do
|
153
|
+
... compute initial value ...
|
154
|
+
... context will be either the class an ...
|
155
|
+
... instance depending on the scope ...
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
Other options to the `state` declaration are:
|
160
|
+
|
161
|
+
+ `scope:` either `:class`, `:instance`, `:shared`. Details below!
|
162
|
+
+ `reader:` either `true`, or a symbol used to declare a reader (getter) method.
|
163
|
+
+ `initializer:` either a Proc or a Symbol (indicating a method), to be used to initialize the state.
|
164
|
+
|
165
|
+
The value of the `scope` option determines where the state resides.
|
166
|
+
|
167
|
+
+ A class state has one instance per class and is directly accessible in class methods, and indirectly in instances using `self.class.state`.
|
168
|
+
+ An instance state has a different copy in each instance of the class, and is not accessible by class methods.
|
169
|
+
+ A shared state is like a class state, but is also directly accessible in instances.
|
170
|
+
|
171
|
+
The default value for `scope:` depends on where the state is declared:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
state :items # declares an instance state variable, each instance gets its own state
|
175
|
+
class << self
|
176
|
+
state :items # declares a class instance state variable
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
In the above example there is one class instance state named `items` and an additional state variable also called
|
181
|
+
items for each instance.
|
182
|
+
|
183
|
+
The `shared` option just makes it easier to access a class state from instances.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class MyStore < Hyperloop::Store
|
187
|
+
state :shared_state, scope: :shared
|
188
|
+
state :class_state, scope: :class
|
189
|
+
state :instance_state # scope: :instance is default here
|
190
|
+
|
191
|
+
def instance_method
|
192
|
+
# shared state makes class states easy to access
|
193
|
+
state.shared_state
|
194
|
+
# without shared state class_state is still accessible
|
195
|
+
# with more typing
|
196
|
+
self.class.class_state
|
197
|
+
# each instance gets its own copy of instance states
|
198
|
+
state.instance_state
|
199
|
+
# attempt to access a declared state variable out of context
|
200
|
+
# results in an error!
|
201
|
+
state.class_state # exception!
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.class_method
|
205
|
+
# this is the same state as was referenced in instance_method
|
206
|
+
state.shared_state
|
207
|
+
# and so is this
|
208
|
+
state.class_state
|
209
|
+
# and this will raise an exception
|
210
|
+
state.instance_state
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
Class state variables are initialized by an implicit `Hyperloop::Application::Boot` receiver. If an initial value is directly provided (not via a proc, method or block) then the value will be `dup`ed when the second and following Boot dispatches are received. The proc, method or block initializers will run in the context of the class, and the state variable will be available. For example:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
state :boot_counter, scope: :shared do
|
218
|
+
(state.boot_counter || 0)+1
|
219
|
+
end
|
220
|
+
|
221
|
+
# more practically perhaps:
|
222
|
+
|
223
|
+
state :my_state, scope: :shared do
|
224
|
+
state.my_state || [] # don't re-initialize me on reboots
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
Instance variables are initialized when instances of the Store are created. Each initialization will `dup` the initial value unless supplied by a proc, method or block.
|
229
|
+
|
230
|
+
This initialization behavior will work in most cases but for more control simply leave off any initializer, and write your own.
|
231
|
+
|
232
|
+
**Note for class states there is a subtle difference between saying:**
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
state my_state: nil, scope: :shared # or :class
|
236
|
+
# and
|
237
|
+
state :my_state, scope: :shared # or :class
|
238
|
+
|
239
|
+
```
|
240
|
+
|
241
|
+
In the first case `my_state` will be re-initialized to nil on every boot, in the second case it will not.
|
242
|
+
|
243
|
+
## Receiving Operation Dispatches
|
244
|
+
|
245
|
+
Stores can receive Operation dispatches using the receive method.
|
246
|
+
|
247
|
+
Here is a simple shopping cart Store that receives Add, Remove and Empty Operations:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
class Cart < Hyperloop::Store
|
251
|
+
# First we will define the two Operations.
|
252
|
+
# Because these are closely associated with the Cart
|
253
|
+
# we will name space them inside the cart.
|
254
|
+
class Add < HyperOperation
|
255
|
+
param :item
|
256
|
+
param :qty, type: Integer, min: 1
|
257
|
+
end
|
258
|
+
class Remove < HyperOperation
|
259
|
+
param :item
|
260
|
+
param :qty, type: Integer, nils: true, min: 1
|
261
|
+
end
|
262
|
+
class Empty < HyperOperation
|
263
|
+
end
|
264
|
+
|
265
|
+
# The cart's state is represented as a hash, items are the keys, qty is the value
|
266
|
+
# initialize the hash by receiving the system Hyperloop::Application::Boot or Empty dispatches
|
267
|
+
|
268
|
+
receives Hyperloop::Application::Boot, Empty do
|
269
|
+
mutate.items(Hash.new { |h, k| h[k] = 0 })
|
270
|
+
end
|
271
|
+
|
272
|
+
# The stores getter (or reader) method
|
273
|
+
|
274
|
+
def self.items
|
275
|
+
state.items
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.empty?
|
279
|
+
state.items.empty?
|
280
|
+
end
|
281
|
+
|
282
|
+
receives Add do
|
283
|
+
# notice we use mutate.items since we are modifying the hash
|
284
|
+
mutate.items[params.item] += params.qty
|
285
|
+
end
|
286
|
+
|
287
|
+
receives Remove do
|
288
|
+
mutate.items[params.item] -= params.qty
|
289
|
+
# remove any items with zero qty from the cart
|
290
|
+
mutate.items.delete(params.item) if state.items[params.item] < 1
|
291
|
+
end
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
This example demonstrates the two ingredients of a Store:
|
296
|
+
|
297
|
+
+ Receiving Operation Dispatches and
|
298
|
+
+ Reading, and Mutating *states*.
|
299
|
+
|
300
|
+
These are explained in detail below.
|
301
|
+
|
302
|
+
The `receive` method takes an list of Operations, and either a symbol (indicating a class method to call), a proc, or a block.
|
303
|
+
|
304
|
+
When the dispatch is received the method, proc, or block will be run within the context of the Store's class (not an instance.) In addition the `params` method from the Operation will be available to access the Operations parameters.
|
305
|
+
|
306
|
+
The *Flux* paradigm promotes only mutating state inside of receivers.
|
307
|
+
|
308
|
+
Hyperloop is less opinionated. You may also add mutator methods to your class. Our recommendation is that you append an exclamation (!) to methods that mutate state.
|
309
|
+
|
310
|
+
Note that it is reasonable to have several receivers for the same Operation. This allows subclassing, mixins, and separation of concerns.
|
311
|
+
|
312
|
+
Note also that the Ruby scoping rules make it very reasonable to define the Operations to be received by a Store inside the Store's scope. This does not change the semantics of either the Store or the Operation, but simply keeps the name space organized.
|