simple-feed 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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