simple-feed 1.0.2

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +14 -0
  7. data/.yardopts +3 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +18 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +457 -0
  12. data/Rakefile +16 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/examples/hash_provider_example.rb +24 -0
  16. data/examples/redis_provider_example.rb +28 -0
  17. data/examples/shared/provider_example.rb +66 -0
  18. data/lib/simple-feed.rb +1 -0
  19. data/lib/simple_feed.rb +1 -0
  20. data/lib/simplefeed.rb +61 -0
  21. data/lib/simplefeed/activity/base.rb +14 -0
  22. data/lib/simplefeed/activity/multi_user.rb +71 -0
  23. data/lib/simplefeed/activity/single_user.rb +70 -0
  24. data/lib/simplefeed/dsl.rb +38 -0
  25. data/lib/simplefeed/dsl/activities.rb +70 -0
  26. data/lib/simplefeed/dsl/formatter.rb +109 -0
  27. data/lib/simplefeed/event.rb +87 -0
  28. data/lib/simplefeed/feed.rb +78 -0
  29. data/lib/simplefeed/providers.rb +45 -0
  30. data/lib/simplefeed/providers/base/provider.rb +84 -0
  31. data/lib/simplefeed/providers/hash.rb +8 -0
  32. data/lib/simplefeed/providers/hash/paginator.rb +31 -0
  33. data/lib/simplefeed/providers/hash/provider.rb +169 -0
  34. data/lib/simplefeed/providers/proxy.rb +38 -0
  35. data/lib/simplefeed/providers/redis.rb +9 -0
  36. data/lib/simplefeed/providers/redis/boot_info.yml +99 -0
  37. data/lib/simplefeed/providers/redis/driver.rb +158 -0
  38. data/lib/simplefeed/providers/redis/provider.rb +255 -0
  39. data/lib/simplefeed/providers/redis/stats.rb +85 -0
  40. data/lib/simplefeed/providers/serialization/key.rb +82 -0
  41. data/lib/simplefeed/response.rb +77 -0
  42. data/lib/simplefeed/version.rb +3 -0
  43. data/man/running-the-example.png +0 -0
  44. data/man/sf-example.png +0 -0
  45. data/simple-feed.gemspec +44 -0
  46. metadata +333 -0
@@ -0,0 +1,255 @@
1
+ require 'redis'
2
+ require 'base62-rb'
3
+ require 'forwardable'
4
+ require 'redis/pipeline' # defines Redis::Future
5
+
6
+ require 'simplefeed/providers/base/provider'
7
+ require 'simplefeed/providers/serialization/key'
8
+
9
+ require_relative 'driver'
10
+ require_relative 'stats'
11
+
12
+ require 'pp'
13
+
14
+ module SimpleFeed
15
+ module Providers
16
+ module Redis
17
+ # Internal data structure:
18
+ #
19
+ # ```YAML
20
+ # u.afkj234.data:
21
+ # - [ 'John liked Robert', '2016-11-20 23:32:56 -0800' ]
22
+ # - [ 'Debbie liked Robert', '2016-11-20 23:35:56 -0800' ]
23
+ # u.afkj234.meta: { total: 2, unread: 2, last_read: 2016-11-20 22:00:34 -08:00 GMT }
24
+ # ```
25
+ class Provider < ::SimpleFeed::Providers::Base::Provider
26
+
27
+ # SimpleFeed::Providers.define_provider_methods(self) do |provider, method, **opts, &block|
28
+ # users = Users.new(provider: provider, user_ids: opts.delete(:user_ids))
29
+ # opts.empty? ?
30
+ # users.send(method, &block) :
31
+ # users.send(method, **opts, &block)
32
+ # end
33
+ #
34
+
35
+ include Driver
36
+
37
+ def store(user_ids:, value:, at: Time.now)
38
+ with_response_pipelined(user_ids) do |redis, key|
39
+ tap redis.zadd(key.data, at.to_f, value) do
40
+ redis.zremrangebyrank(key.data, 0, -feed.max_size - 1)
41
+ end
42
+ end
43
+ end
44
+
45
+ def delete(user_ids:, value:, **)
46
+ with_response_pipelined(user_ids) do |redis, key|
47
+ redis.zrem(key.data, value)
48
+ end
49
+ end
50
+
51
+ def delete_if(user_ids:)
52
+ raise ArgumentError, '#delete_if must be called with a block that receives (user_id, event) as arguments.' unless block_given?
53
+ with_response_batched(user_ids) do |key|
54
+ fetch(user_ids: [key.user_id])[key.user_id].each do |event|
55
+ with_redis do |redis|
56
+ yield(key.user_id, event) ? redis.zrem(key.data, event.value) : false
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def wipe(user_ids:)
63
+ with_response_pipelined(user_ids) do |redis, key|
64
+ should_wipe = block_given? ? yield(key.user_id) : true
65
+ key.keys.all? { |redis_key| redis.del(redis_key) if should_wipe }
66
+ end
67
+ end
68
+
69
+ def paginate(user_ids:, page:, per_page: feed.per_page, peek: false, with_total: false)
70
+ reset_last_read(user_ids: user_ids) unless peek
71
+ with_response_pipelined(user_ids) do |redis, key|
72
+ events = paginated_events(page, per_page, redis, key)
73
+ with_total ? { events: events,
74
+ total_count: redis.zcard(key.data) } : events
75
+ end
76
+ end
77
+
78
+
79
+ def fetch(user_ids:)
80
+ with_response_pipelined(user_ids) do |redis, key|
81
+ redis.zrevrange(key.data, 0, -1, withscores: true)
82
+ end
83
+ end
84
+
85
+ def reset_last_read(user_ids:, at: Time.now)
86
+ with_response_pipelined(user_ids) do |redis, key, *|
87
+ reset_users_last_read(redis, key, at.to_f)
88
+ end
89
+ end
90
+
91
+ def total_count(user_ids:)
92
+ with_response_pipelined(user_ids) do |redis, key|
93
+ redis.zcard(key.data)
94
+ end
95
+ end
96
+
97
+ def unread_count(user_ids:)
98
+ response = with_response_pipelined(user_ids) do |redis, key|
99
+ get_users_last_read(redis, key)
100
+ end
101
+ with_response_pipelined(response.user_ids, response) do |redis, key, _response|
102
+ last_read = _response.delete(key.user_id).to_f
103
+ redis.zcount(key.data, last_read, '+inf')
104
+ end
105
+ end
106
+
107
+ def last_read(user_ids:)
108
+ with_response_pipelined(user_ids) do |redis, key, *|
109
+ get_users_last_read(redis, key)
110
+ end
111
+ end
112
+
113
+ FEED_METHODS = %i(total_memory_bytes total_users last_disk_save_time)
114
+
115
+ def total_memory_bytes
116
+ with_stats(:used_memory_since_boot)
117
+ end
118
+
119
+ def total_users
120
+ with_redis { |redis| redis.dbsize / 2 }
121
+ end
122
+
123
+ def with_stats(operation)
124
+ with_redis do |redis|
125
+ SimpleFeed::Providers::Redis::Stats.new(redis).send(operation)
126
+ end
127
+ end
128
+
129
+ def transform_response(user_id = nil, result)
130
+ case result
131
+ when ::Redis::Future
132
+ transform_response(user_id, result.value)
133
+
134
+ when ::Hash
135
+
136
+ if result.values.any? { |v| transformable_type?(v) }
137
+ result.each { |k, v| result[k] = transform_response(user_id, v) }
138
+ else
139
+ result
140
+ end
141
+
142
+ when ::Array
143
+
144
+ if result.any? { |v| transformable_type?(v) }
145
+ result = result.map { |v| transform_response(user_id, v) }
146
+ end
147
+
148
+ if result.size == 2 && result[1].is_a?(Float)
149
+ SimpleFeed::Event.new(value: result[0], at: Time.at(result[1]))
150
+ else
151
+ result
152
+ end
153
+
154
+ when ::String
155
+ if result =~ /^\d+\.\d+$/
156
+ result.to_f
157
+ elsif result =~ /^\d+$/
158
+ result.to_i
159
+ else
160
+ result
161
+ end
162
+ else
163
+ result
164
+ end
165
+ end
166
+
167
+ def transformable_type?(value)
168
+ [
169
+ ::Redis::Future,
170
+ ::Hash,
171
+ ::Array,
172
+ ::String
173
+ ].include?(value.class)
174
+ end
175
+
176
+ private
177
+
178
+ #——————————————————————————————————————————————————————————————————————————————————————
179
+ # helpers
180
+ #——————————————————————————————————————————————————————————————————————————————————————
181
+
182
+ def reset_users_last_read(redis, key, time = nil)
183
+ time = time.nil? ? Time.now.to_f : time.to_f
184
+ redis.hset(key.meta, 'last_read', time)
185
+ Time.at(time)
186
+ end
187
+
188
+ # returns a string containing a float, which must then be
189
+ # converted into float in #transform
190
+ def get_users_last_read(redis, key)
191
+ redis.hget(key.meta, 'last_read')
192
+ end
193
+
194
+ def paginated_events(page, per_page, redis, key)
195
+ redis.zrevrange(key.data, (page - 1) * per_page, page * per_page - 1, withscores: true)
196
+ end
197
+
198
+ #——————————————————————————————————————————————————————————————————————————————————————
199
+ # Operations with response
200
+ #——————————————————————————————————————————————————————————————————————————————————————
201
+
202
+ def with_response_pipelined(user_ids, response = nil)
203
+ with_response(response) do |response|
204
+ batch_pipelined(user_ids) do |redis, key|
205
+ response.for(key.user_id) { yield(redis, key, response) }
206
+ end
207
+ end
208
+ end
209
+
210
+ def with_response_multi(user_ids, response = nil)
211
+ with_response(response) do |response|
212
+ batch_multi(user_ids) do |redis, key|
213
+ response.for(key.user_id) { yield(redis, key, response) }
214
+ end
215
+ end
216
+ end
217
+
218
+ #——————————————————————————————————————————————————————————————————————————————————————
219
+ # Batch operations
220
+ #——————————————————————————————————————————————————————————————————————————————————————
221
+ def batch_pipelined(user_ids)
222
+ to_array(user_ids).each_slice(batch_size) do |batch|
223
+ with_pipelined do |redis|
224
+ batch.each do |user_id|
225
+ yield(redis, key(user_id))
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ def batch_pipelined_multi(user_ids)
232
+ to_array(user_ids).each_slice(batch_size) do |batch|
233
+ with_pipelined do
234
+ batch.each do |user_id|
235
+ with_multi do |redis|
236
+ yield(redis, key(user_id))
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ def batch_multi(user_ids)
244
+ to_array(user_ids).each_slice(batch_size) do |batch|
245
+ batch.each do |user_id|
246
+ with_multi do |redis|
247
+ yield(redis, key(user_id))
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,85 @@
1
+ require 'redis'
2
+ require 'hashie/mash'
3
+ require 'yaml'
4
+
5
+ module SimpleFeed
6
+ module Providers
7
+ module Redis
8
+ class Stats
9
+
10
+ attr_accessor :redis
11
+
12
+ def initialize(redis)
13
+ self.redis = redis
14
+ end
15
+
16
+ def info
17
+ self.class.destringify(redis.info)
18
+ end
19
+
20
+ def boot_info
21
+ self.class.boot_info
22
+ end
23
+
24
+ @boot_info = nil
25
+
26
+ class << self
27
+ attr_accessor :boot_info
28
+
29
+ # Converts strings values of a hash into floats or integers,
30
+ # if the string matches a corresponding pattern.
31
+ def destringify(hash)
32
+ db_hash = {}
33
+ hash.each_pair do |key, value|
34
+ if key =~ /^db\d+$/
35
+ h = {}
36
+ value.split(/,/).each do |word|
37
+ *words = word.split(/=/)
38
+ h[words[0]] = words[1]
39
+ end
40
+ destringify(h)
41
+ db_hash[key.gsub(/^db/, '').to_i] = h
42
+ hash.delete(key)
43
+ else
44
+ hash[key] =
45
+ if value =~ /^-?\d+$/
46
+ value.to_i
47
+ elsif value =~ /^-?\d*\.\d+$/
48
+ value.to_f
49
+ else
50
+ value
51
+ end
52
+ end
53
+ end
54
+ hash[:dbstats] = db_hash unless db_hash.empty?
55
+ Hashie::Mash.new(hash)
56
+ end
57
+
58
+ def load_boot_stats!
59
+ @boot_info ||= destringify(YAML.load(File.open(File.expand_path('../boot_info.yml', __FILE__))))
60
+ end
61
+
62
+ end
63
+
64
+ load_boot_stats!
65
+
66
+ boot_info.keys.each do |key|
67
+ unless key.to_s =~ /^db[0-9]+/
68
+
69
+ define_method(key.to_sym) do
70
+ info[key]
71
+ end
72
+
73
+ define_method("#{key}_at_boot".to_sym) do
74
+ boot_info[key]
75
+ end
76
+
77
+ define_method("#{key}_since_boot".to_sym) do
78
+ info[key] - boot_info[key]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,82 @@
1
+ require 'base62-rb'
2
+ require 'hashie/mash'
3
+ module SimpleFeed
4
+ module Providers
5
+ module Serialization
6
+ class Key
7
+ attr_accessor :user_id, :short_id, :prefix, :config
8
+
9
+ # This hash defines the paramters for the strategy we use
10
+ # to create a compact key based on the user id, feed's namespace,
11
+ # and the functionality, such as 'm' meta — as a hash for storing
12
+ # arbitrary values (in particular, +last_read+). and 'd' data —
13
+ # as a sorted set in for storing the actual events.
14
+ #
15
+ # ### Examples
16
+ #
17
+ # Here is a meta key for a given user ID:
18
+ #
19
+ # user 'm' for meta
20
+ # ↓ ↓
21
+ # "ff|u.f23098.m"
22
+ # ↑ ↑
23
+ # namespace user_id(base62)
24
+ #
25
+ KEY_CONFIG = Hashie::Mash.new({
26
+ separator: '.',
27
+ prefix: '',
28
+ namespace_divider: '|',
29
+ namespace: nil,
30
+ primary: ->(user_id) { ::Base62.encode(user_id) },
31
+ primary_marker: 'u',
32
+ secondary_markers: {
33
+ data: 'd',
34
+ meta: 'm'
35
+ }
36
+ })
37
+
38
+ def initialize(user_id, namespace = nil, optional_key_config = {})
39
+ optional_key_config.merge!(namespace: namespace) if namespace
40
+ self.config = KEY_CONFIG.dup.merge!(optional_key_config)
41
+
42
+ self.user_id = user_id
43
+ self.short_id = config.primary[user_id]
44
+
45
+ self.prefix = configure_prefix
46
+
47
+ config.secondary_markers.each_pair do |type, character|
48
+ self.class.send(:define_method, type) do
49
+ instance_variable_get("@#{type}") || instance_variable_set("@#{type}", "#{prefix}#{config.separator}#{character}")
50
+ end
51
+ end
52
+ end
53
+
54
+ def keys
55
+ config.secondary_markers.map { |k, v| [k, self.send(k)] }
56
+ end
57
+
58
+ def for(type)
59
+ "#{prefix}#{config.separator}#{type.to_s}"
60
+ end
61
+
62
+ def to_s
63
+ super.gsub(/SimpleFeed::Providers::Serialization/, '...*') + { user_id: user_id, short_id: short_id, keys: keys }.to_json
64
+ end
65
+
66
+ def inspect
67
+
68
+ end
69
+
70
+ private
71
+
72
+ # eg. ff|u.123498
73
+ def configure_prefix
74
+ namespace = config.namespace ? "#{config.namespace}#{config.namespace_divider}" : ''
75
+ "#{namespace}#{config.primary_marker}#{config.separator}#{short_id}"
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,77 @@
1
+ require 'hashie'
2
+ require 'forwardable'
3
+ require 'simplefeed'
4
+
5
+ module SimpleFeed
6
+ class Response
7
+ extend Forwardable
8
+
9
+ def_delegators :@result, :delete, :key?, :value?, :values, :keys, :size, :merge!
10
+
11
+ include Enumerable
12
+
13
+ def each
14
+ if block_given?
15
+ @result.each_pair do |user_id, result|
16
+ yield(user_id, result)
17
+ end
18
+ else
19
+ @result.keys.to_enum
20
+ end
21
+ end
22
+
23
+ def initialize(data = {})
24
+ @result = data.dup
25
+ end
26
+
27
+ def for(key_or_user_id, result = nil)
28
+ user_id = key_or_user_id.is_a?(SimpleFeed::Providers::Serialization::Key) ?
29
+ key_or_user_id.user_id :
30
+ key_or_user_id
31
+
32
+ @result[user_id] = result ? result : yield(@result[user_id])
33
+ end
34
+
35
+ def user_ids
36
+ @result.keys
37
+ end
38
+
39
+ def has_user?(user_id)
40
+ @result.key?(user_id)
41
+ end
42
+
43
+ def user_count
44
+ @result.size
45
+ end
46
+
47
+ def to_h
48
+ @result.to_h
49
+ end
50
+
51
+ # Passes results assigned to each user to a transformation
52
+ # function that in turn must return a transformed value for
53
+ # an individual response, and be implemented in the subclasses
54
+ def transform
55
+ if block_given?
56
+ @result.each_pair do |user_id, value|
57
+ @result[user_id] = yield(user_id, value)
58
+ end
59
+ end
60
+ self
61
+ end
62
+
63
+ def result(user_id = nil)
64
+ if user_id then
65
+ @result[user_id]
66
+ else
67
+ if @result.values.size == 1
68
+ @result.values.first
69
+ else
70
+ @result.to_hash
71
+ end
72
+ end
73
+ end
74
+
75
+ alias_method :[], :result
76
+ end
77
+ end