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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +14 -0
- data/.yardopts +3 -0
- data/Gemfile +4 -0
- data/Guardfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +457 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/hash_provider_example.rb +24 -0
- data/examples/redis_provider_example.rb +28 -0
- data/examples/shared/provider_example.rb +66 -0
- data/lib/simple-feed.rb +1 -0
- data/lib/simple_feed.rb +1 -0
- data/lib/simplefeed.rb +61 -0
- data/lib/simplefeed/activity/base.rb +14 -0
- data/lib/simplefeed/activity/multi_user.rb +71 -0
- data/lib/simplefeed/activity/single_user.rb +70 -0
- data/lib/simplefeed/dsl.rb +38 -0
- data/lib/simplefeed/dsl/activities.rb +70 -0
- data/lib/simplefeed/dsl/formatter.rb +109 -0
- data/lib/simplefeed/event.rb +87 -0
- data/lib/simplefeed/feed.rb +78 -0
- data/lib/simplefeed/providers.rb +45 -0
- data/lib/simplefeed/providers/base/provider.rb +84 -0
- data/lib/simplefeed/providers/hash.rb +8 -0
- data/lib/simplefeed/providers/hash/paginator.rb +31 -0
- data/lib/simplefeed/providers/hash/provider.rb +169 -0
- data/lib/simplefeed/providers/proxy.rb +38 -0
- data/lib/simplefeed/providers/redis.rb +9 -0
- data/lib/simplefeed/providers/redis/boot_info.yml +99 -0
- data/lib/simplefeed/providers/redis/driver.rb +158 -0
- data/lib/simplefeed/providers/redis/provider.rb +255 -0
- data/lib/simplefeed/providers/redis/stats.rb +85 -0
- data/lib/simplefeed/providers/serialization/key.rb +82 -0
- data/lib/simplefeed/response.rb +77 -0
- data/lib/simplefeed/version.rb +3 -0
- data/man/running-the-example.png +0 -0
- data/man/sf-example.png +0 -0
- data/simple-feed.gemspec +44 -0
- 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
|