simple-feed 1.0.2 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +1 -0
- data/README.md +60 -110
- data/examples/shared/provider_example.rb +21 -20
- data/lib/simplefeed.rb +0 -8
- data/lib/simplefeed/activity/multi_user.rb +21 -9
- data/lib/simplefeed/activity/single_user.rb +34 -23
- data/lib/simplefeed/dsl.rb +4 -4
- data/lib/simplefeed/dsl/formatter.rb +30 -23
- data/lib/simplefeed/event.rb +8 -15
- data/lib/simplefeed/providers/hash/provider.rb +25 -8
- data/lib/simplefeed/providers/redis/driver.rb +32 -2
- data/lib/simplefeed/providers/redis/provider.rb +14 -3
- data/lib/simplefeed/version.rb +1 -1
- data/man/activity-feed-action.png +0 -0
- data/man/running-example-redis-debug.png +0 -0
- data/man/running-example.png +0 -0
- data/simple-feed.gemspec +1 -3
- metadata +6 -19
- data/man/running-the-example.png +0 -0
- data/man/sf-example.png +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37f4c35a87e3c3ed5b0a3cbdc88b6a88edc795c3
|
4
|
+
data.tar.gz: c88031baedfb60e6376d561621d2547b2db37978
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb887ba71541e037975ae5800842b2f5a824901c5134ad6e38cd91489917fb62971edefac865fa5d2f7e21452417d08c77d164caa84d84eb16c0f681bb12a239
|
7
|
+
data.tar.gz: db3fad51130186c6199e6874b54e4659eedada7cf4e995781a5103e720e6beb712e0a610d30f5a2eed9abc9f8b23336f33007539e3825dc2ee3dad5901acce79
|
data/.codeclimate.yml
CHANGED
data/.rspec
CHANGED
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -7,42 +7,43 @@
|
|
7
7
|
[![Test Coverage](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/coverage.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/coverage)
|
8
8
|
[![Issue Count](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/issue_count.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
|
9
9
|
|
10
|
-
This is a ruby implementation of
|
10
|
+
This is a fast, pure-ruby implementation of an activity feed concept commonly used in social networking applications. The implementation is optimized for **read-time performance** and high concurrency (lots of users), and can be extended with custom backend providers. Two providers come bundled: the production-ready Redis provider, and a naive pure Hash-based provider.
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
__Important Notes and Acknowledgements:__
|
13
|
+
|
14
|
+
* SimpleFeed *does not depend on Ruby on Rails* and is a __pure-ruby__ implementation.
|
15
|
+
* __SimpleFeed requires ruby 2.3 or later.__
|
16
|
+
* SimpleFeed is currently live in production.
|
17
|
+
* We'd like to thank __[Simbi, Inc — Symbiotic Economy](http://simbi.com)__ for their sponsorship of the development of this open source library.
|
17
18
|
|
18
19
|
## What is an activity feed?
|
19
20
|
|
20
21
|
> Activity feed is a visual representation of a time-ordered, reverse chronological list of events which can be:
|
21
22
|
>
|
22
23
|
> * personalized for a given user or a group, or global
|
23
|
-
> * filtered by a certain characteristic, such as, eg.
|
24
|
-
> * the source of the events — i.e. people you follow
|
25
|
-
> * type of event (i.e. posts, likes, and updates)
|
26
|
-
> * the target of the event i.e. my own activity as opposed to from those I follow.
|
27
24
|
> * aggregated across several actors for a similar event type, eg. "John, Mary, etc.. followed George"
|
25
|
+
> * filtered by a certain characteristic, such as:
|
26
|
+
> * the actor producing an event — i.e. people you follow on a social network, or "yourself" for your own activity
|
27
|
+
> * the type of an event (i.e. posts, likes, comments, stories, etc)
|
28
|
+
> * the target of an event (commonly a user, but can also be a thing you are interested in, e.g. a github repo you are watching)
|
28
29
|
|
29
|
-
Here is an example of a
|
30
|
+
Here is an example of a real feed powered by this library, and which is very common on today's social media sites:
|
30
31
|
|
31
|
-
[![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/
|
32
|
+
[![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/activity-feed-action.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/activity-feed-action.png)
|
32
33
|
|
33
|
-
|
34
|
-
library, therefore to integrate with SimpleFeed requires implementing
|
35
|
-
several _glue points_ in your code.
|
34
|
+
What you publish into your feed — i.e. _stories_ or _events_, will depend entirely on your application. SimpleFeed should be able to power the most demanding *write-time* feeds.
|
36
35
|
|
37
|
-
## Challenges
|
36
|
+
## Challenges
|
38
37
|
|
39
|
-
|
38
|
+
Building a personalized activity feeds tend to be a challenging task, due to the diversity of event types that it often includes, the personalization requirement, and the need for it to often scale to very large numbers of concurrent users. Therefore common implementations tend to focus on either:
|
40
39
|
|
41
40
|
* optimizing the read time performance by pre-computing the feed for each user ahead of time
|
42
41
|
* OR optimizing the various ranking algorithms by computing the feed at read time, with complex forms of caching addressing the performance requirements.
|
43
42
|
|
44
43
|
The first type of feed is much simpler to implement on a large scale (up to a point), and it scales well if the data is stored in a light-weight in-memory storage such as Redis. This is exactly the approach this library takes.
|
45
44
|
|
45
|
+
For more information about various types of feed, and the typical architectures that power them — please read ["How would you go about building an activity feed like Facebook?"](https://hashnode.com/post/architecture-how-would-you-go-about-building-an-activity-feed-like-facebook-cioe6ea7q017aru53phul68t1/answer/ciol0lbaa02q52s530vfqea0t) by [Lee Byron](https://hashnode.com/@leebyron).
|
46
|
+
|
46
47
|
## Overview
|
47
48
|
|
48
49
|
The feed library aims to address the following goals:
|
@@ -103,6 +104,9 @@ activity.store(value: 'hello')
|
|
103
104
|
|
104
105
|
# or equivalent:
|
105
106
|
@event = SimpleFeed::Event.new(value: 'hello', at: Time.now)
|
107
|
+
# or even simpler:
|
108
|
+
@event = SimpleFeed::Event.new('hello', Time.now)
|
109
|
+
# and then:
|
106
110
|
activity.store(event: @event)
|
107
111
|
# => false # false indicates that the same event is already in the feed.
|
108
112
|
```
|
@@ -122,7 +126,7 @@ activity.paginate(page: 1)
|
|
122
126
|
|
123
127
|
The feed API is offered in a single-user and a batch (multi-user) forms.
|
124
128
|
|
125
|
-
The
|
129
|
+
The main and only difference is in what the methods return. In the
|
126
130
|
single user case, the return of, say, `#total_count` is an `Integer`
|
127
131
|
value representing the total count for this user.
|
128
132
|
|
@@ -134,56 +138,44 @@ Please see further below the details about the [Batch API](#bach-api).
|
|
134
138
|
|
135
139
|
<a name="single-user-api"/>
|
136
140
|
|
137
|
-
#####
|
141
|
+
##### Single-User API
|
138
142
|
|
139
|
-
|
140
|
-
via the `SimpleFeed::Feed#for` instance method. Optimized for simplicity
|
141
|
-
of data retrieval of a single-user, this method strives for simplicity
|
142
|
-
and ease of use.
|
143
|
+
In the examples below we show responses based on a single-user usage. As previously mentioned, the multi-user case is the same, except for the response values, and is discussed further below.
|
143
144
|
|
144
|
-
Below is a user session that demonstrates simple return values from the
|
145
|
-
Feed operations for a given user:
|
145
|
+
Below is a user session that demonstrates simple return values from the feed operations for a given user:
|
146
146
|
|
147
147
|
```ruby
|
148
148
|
require 'simplefeed'
|
149
149
|
|
150
150
|
# Define the feed using an in-memory Hash provider, which uses
|
151
151
|
# SortedSet to keep user's events sorted.
|
152
|
-
SimpleFeed.define(:
|
152
|
+
SimpleFeed.define(:followers) do |f|
|
153
153
|
f.provider = SimpleFeed.provider(:hash)
|
154
154
|
f.per_page = 50
|
155
155
|
f.per_page = 2
|
156
156
|
end
|
157
157
|
|
158
158
|
# Let's get the Activity instance that wraps this user_id
|
159
|
-
activity = SimpleFeed.get(:
|
160
|
-
# => [... complex object removed for brevity ]
|
161
|
-
|
159
|
+
activity = SimpleFeed.get(:followers).activity(user_id) # => [... complex object removed for brevity ]
|
162
160
|
# let's clear out this feed to ensure it's empty
|
163
|
-
activity.wipe
|
164
|
-
# => true
|
165
|
-
|
161
|
+
activity.wipe # => true
|
166
162
|
# Let's verify that the counts for this feed are at zero
|
167
|
-
activity.total_count
|
168
|
-
|
169
|
-
|
170
|
-
activity.unread_count
|
171
|
-
#=> 0
|
172
|
-
|
163
|
+
activity.total_count # => 0
|
164
|
+
activity.unread_count # => 0
|
173
165
|
# Store some events
|
174
|
-
activity.store(value: 'hello')
|
175
|
-
activity.store(value: 'goodbye')
|
176
|
-
|
166
|
+
activity.store(value: 'hello') # => true
|
167
|
+
activity.store(value: 'goodbye') # => true
|
168
|
+
activity.unread_count # => 2
|
177
169
|
# Now we can paginate the events, which by default resets "last_read" timestamp the user
|
178
170
|
activity.paginate(page: 1)
|
179
171
|
# [
|
180
172
|
# [0] #<SimpleFeed::Event#70138821650220 {"value":"goodbye","at":1480475294.0579991}>,
|
181
173
|
# [1] #<SimpleFeed::Event#70138821649420 {"value":"hello","at":1480475294.057138}>
|
182
174
|
# ]
|
183
|
-
|
184
175
|
# Now the unread_count should return 0 since the user just "viewed" the feed.
|
185
|
-
activity.unread_count
|
186
|
-
|
176
|
+
activity.unread_count # => 0
|
177
|
+
activity.delete(value: 'hello') # => true
|
178
|
+
activity.total_count # => 1
|
187
179
|
```
|
188
180
|
|
189
181
|
You can fetch all items in the feed using `#fetch`, and you can
|
@@ -252,6 +244,9 @@ with_activity(activity, countries: data_to_store) do
|
|
252
244
|
store(value: country) { |result| report(result ? 'success' : 'failure') }
|
253
245
|
# we can call #report inside the proc because it is evaluated in the
|
254
246
|
# outside context of the #with_activity
|
247
|
+
|
248
|
+
# now let's print a color ASCII dump of the entire feed for this user:
|
249
|
+
color_dump
|
255
250
|
end
|
256
251
|
printf "Activity counts are: %d unread of %d total\n", unread_count, total_count
|
257
252
|
end
|
@@ -261,56 +256,9 @@ end
|
|
261
256
|
|
262
257
|
## Complete API
|
263
258
|
|
264
|
-
|
265
|
-
|
266
|
-
For a single user, via the instance of
|
267
|
-
`SimpleFeed::Activity::UserActivity` class:
|
268
|
-
|
269
|
-
```ruby
|
270
|
-
require 'simplefeed'
|
259
|
+
For completeness sake we'll show the multi-user API responses only. For a single-user use-case the response is typically a scalar, and the input is a singular `user_id`, not an array of ids.
|
271
260
|
|
272
|
-
|
273
|
-
|
274
|
-
@ua.store(event:)
|
275
|
-
@ua.store(value:, at:)
|
276
|
-
# => [Boolean] true if the value was stored, false if it wasn't.
|
277
|
-
|
278
|
-
@ua.delete(event:)
|
279
|
-
@ua.delete(value:, at:)
|
280
|
-
# => [Boolean] true if the value was removed, false if it didn't exist
|
281
|
-
|
282
|
-
@ua.delete_if do |user_id, event|
|
283
|
-
# if the block returns true, the event is deleted
|
284
|
-
end
|
285
|
-
|
286
|
-
@ua.wipe
|
287
|
-
# => [Boolean] true
|
288
|
-
|
289
|
-
# Options:
|
290
|
-
# with options[:peak] = true it does not reset last_read
|
291
|
-
# with options[:with_total] = true it returns a hash with a total:
|
292
|
-
# @return:
|
293
|
-
@ua.paginate(page:, per_page:, **options)
|
294
|
-
# @return: [Array]<Event> (without options[:with_total])
|
295
|
-
# @return: { events: [Array]<Event, total_count: 3242 }
|
296
|
-
|
297
|
-
@ua.fetch
|
298
|
-
# => [Array]<Event> – returns all events up to Feed.max_size
|
299
|
-
|
300
|
-
@ua.reset_last_read
|
301
|
-
# => [Time] last_read
|
302
|
-
|
303
|
-
@ua.total_count
|
304
|
-
# => [Integer] total_count
|
305
|
-
|
306
|
-
@ua.unread_count
|
307
|
-
# => [Integer] unread_count
|
308
|
-
|
309
|
-
@ua.last_read
|
310
|
-
# => [Time] last_read
|
311
|
-
```
|
312
|
-
|
313
|
-
#### Batch User API
|
261
|
+
#### Multi-User (Batch) API
|
314
262
|
|
315
263
|
Each API call at this level expects an array of user IDs, therefore the
|
316
264
|
return value is an object, `SimpleFeed::Response`, containing individual
|
@@ -331,15 +279,24 @@ responses for each user, accessible via `response[user_id]` method.
|
|
331
279
|
# if the block returns true, the event is deleted
|
332
280
|
end
|
333
281
|
|
282
|
+
# Wipe the feed for a given user(s)
|
334
283
|
@multi.wipe
|
335
284
|
# => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
|
336
285
|
|
337
|
-
|
286
|
+
# Return a paginated list of all items, optionally with the total count of items
|
287
|
+
@multi.paginate(page:, per_page:, peek: false, with_total: false)
|
338
288
|
# => [Response] { user_id => [Array]<Event>, ... }
|
339
|
-
#
|
289
|
+
# Options:
|
290
|
+
# peek: true — does not reset last_read, otherwise it does.
|
291
|
+
# with_total: true — returns a hash for each user_id:
|
292
|
+
# => [Response] { user_id => { events: Array<Event>, total_count: 3 }, ... }
|
340
293
|
|
341
|
-
|
294
|
+
# Return un-paginated list of all items, optionally filtered
|
295
|
+
@multi.fetch(since: nil)
|
342
296
|
# => [Response] { user_id => [Array]<Event>, ... }
|
297
|
+
# Options:
|
298
|
+
# since: <timestamp> — if provided, returns all items posted since then
|
299
|
+
# since: :unread — if provided, returns all unread items and resets +last_read+
|
343
300
|
|
344
301
|
@multi.reset_last_read
|
345
302
|
# => [Response] { user_id => [Time] last_read, ... }
|
@@ -376,11 +333,11 @@ Two providers are available with this gem:
|
|
376
333
|
|
377
334
|
### Redis Provider
|
378
335
|
|
379
|
-
If you set environment variable `REDIS_DEBUG` and run the example (see below) you will see every operation redis performs. This could be useful in debugging an issue or submitting a bug report.
|
336
|
+
If you set environment variable `REDIS_DEBUG` to `true` and run the example (see below) you will see every operation redis performs. This could be useful in debugging an issue or submitting a bug report.
|
380
337
|
|
381
|
-
## Examples
|
338
|
+
## Running the Examples
|
382
339
|
|
383
|
-
Source code for the gem contains the `examples` folder with an example file that can be used to
|
340
|
+
Source code for the gem contains the `examples` folder with an example file that can be used to test out the providers, and see what they do under the hood.
|
384
341
|
|
385
342
|
To run it, checkout the source of the library, and then:
|
386
343
|
|
@@ -392,20 +349,13 @@ be rspec # make sure tests are passing
|
|
392
349
|
ruby examples/redis_provider_example.rb
|
393
350
|
```
|
394
351
|
|
395
|
-
The above command will help you download, setup all dependencies, and run the examples for a single user
|
352
|
+
The above command will help you download, setup all dependencies, and run the examples for a single user:
|
396
353
|
|
397
|
-
|
398
|
-
ruby examples/redis_provider_example.rb 10
|
399
|
-
```
|
400
|
-
|
401
|
-
Or to measure the time:
|
402
|
-
```bash
|
403
|
-
time ruby examples/redis_provider_example.rb 1000 > /dev/null
|
404
|
-
```
|
354
|
+
[![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example.png)
|
405
355
|
|
406
|
-
|
356
|
+
If you set `REDIS_DEBUG` variable prior to running the example, you will be able to see every single Redis command executed as the example works its way through. Below is a sample output:
|
407
357
|
|
408
|
-
[![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-
|
358
|
+
[![Example with Debugging](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example-redis-debug.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example-redis-debug.png)
|
409
359
|
|
410
360
|
### Generating Ruby API Documentation
|
411
361
|
|
@@ -24,43 +24,44 @@ def p(*args)
|
|
24
24
|
end
|
25
25
|
|
26
26
|
with_activity(@activity) do
|
27
|
-
header "#{@activity.feed.provider_type.to_s} provider example"
|
28
|
-
wipe { puts 'wiping feed...' }
|
29
27
|
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
header "#{@activity.feed.provider_type} provider example"
|
29
|
+
|
30
|
+
wipe
|
31
|
+
|
32
|
+
store('value one') { p 'storing new value', 'value one' }
|
33
|
+
store('value two') { p 'storing new value', 'value two' }
|
34
|
+
store('value three') { p 'storing new value', 'value three' }
|
35
|
+
|
33
36
|
hr
|
34
37
|
|
35
38
|
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
36
39
|
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
37
40
|
|
38
|
-
header '
|
39
|
-
paginate(page: 1, per_page: 2) { |r| puts r[@uid].map(&:
|
40
|
-
header '
|
41
|
-
paginate(page: 2, per_page: 2) { |r| puts r[@uid].map(&:
|
42
|
-
header 'LAST PAGE (PER-PAGE: 1) #to_color_s'
|
43
|
-
paginate(page: 3, per_page: 1) { |r| puts r[@uid].map(&:to_color_s) }
|
41
|
+
header 'paginate(page: 1, per_page: 2)'
|
42
|
+
paginate(page: 1, per_page: 2) { |r| puts r[@uid].map(&:to_color_s) }
|
43
|
+
header 'paginate(page: 2, per_page: 2)'
|
44
|
+
paginate(page: 2, per_page: 2) { |r| puts r[@uid].map(&:to_color_s) }
|
44
45
|
|
45
46
|
hr
|
46
|
-
|
47
|
-
|
47
|
+
|
48
|
+
total_count { |r| p 'total_count ', "#{r[@uid]._v}" }
|
49
|
+
unread_count { |r| p 'unread_count ', "#{r[@uid]._v}" }
|
48
50
|
|
49
51
|
hr
|
50
52
|
store('value four') { p 'storing', 'value four' }
|
51
|
-
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
52
|
-
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
53
53
|
|
54
54
|
color_dump
|
55
55
|
|
56
|
-
|
56
|
+
header 'deleting'
|
57
|
+
|
57
58
|
delete('value three') { p 'deleting', 'value three' }
|
58
|
-
total_count { |r| p 'total_count
|
59
|
-
unread_count { |r| p 'unread_count
|
59
|
+
total_count { |r| p 'total_count ', "#{r[@uid]._v}" }
|
60
|
+
unread_count { |r| p 'unread_count ', "#{r[@uid]._v}" }
|
60
61
|
hr
|
61
62
|
delete('value four') { p 'deleting', 'value four' }
|
62
|
-
total_count { |r| p 'total_count
|
63
|
-
unread_count { |r| p 'unread_count
|
63
|
+
total_count { |r| p 'total_count ', "#{r[@uid]._v}" }
|
64
|
+
unread_count { |r| p 'unread_count ', "#{r[@uid]._v}" }
|
64
65
|
|
65
66
|
end
|
66
67
|
|
data/lib/simplefeed.rb
CHANGED
@@ -7,14 +7,6 @@ require 'simplefeed/providers/redis'
|
|
7
7
|
require 'simplefeed/providers/hash'
|
8
8
|
require 'simplefeed/dsl'
|
9
9
|
|
10
|
-
Float.class_eval do
|
11
|
-
def near_eql?(other)
|
12
|
-
delta = 0.001
|
13
|
-
n = (other - self).abs
|
14
|
-
delta >= n
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
10
|
module SimpleFeed
|
19
11
|
@registry = {}
|
20
12
|
|
@@ -13,10 +13,11 @@ module SimpleFeed
|
|
13
13
|
include Enumerable
|
14
14
|
|
15
15
|
#
|
16
|
-
#
|
16
|
+
# API Examples
|
17
|
+
# ============
|
17
18
|
#
|
18
|
-
|
19
|
-
# @multi = SimpleFeed.get(:feed_name).
|
19
|
+
#```ruby
|
20
|
+
# @multi = SimpleFeed.get(:feed_name).activity(User.active.map(&:id))
|
20
21
|
#
|
21
22
|
# @multi.store(value:, at:)
|
22
23
|
# @multi.store(event:)
|
@@ -24,18 +25,28 @@ module SimpleFeed
|
|
24
25
|
#
|
25
26
|
# @multi.delete(value:, at:)
|
26
27
|
# @multi.delete(event:)
|
27
|
-
# # => [Response] { user_id => [Boolean], ... } true if the value was
|
28
|
+
# # => [Response] { user_id => [Boolean], ... } true if the value was removed, false if it didn't exist
|
29
|
+
#
|
30
|
+
# @multi.delete_if do |user_id, event|
|
31
|
+
# # if the block returns true, the event is deleted
|
32
|
+
# end
|
28
33
|
#
|
29
34
|
# @multi.wipe
|
30
35
|
# # => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
|
31
36
|
#
|
32
|
-
# @multi.paginate(page:, per_page:, peek: false)
|
37
|
+
# @multi.paginate(page:, per_page:, peek: false, with_total: false)
|
33
38
|
# # => [Response] { user_id => [Array]<Event>, ... }
|
39
|
+
# # Options:
|
40
|
+
# # peek: true — does not reset last_read, otherwise it does.
|
41
|
+
# # with_total: true — returns a hash for each user_id:
|
42
|
+
# # => [Response] { user_id => { events: Array<Event>, total_count: 3 }, ... }
|
34
43
|
#
|
35
|
-
# #
|
36
|
-
#
|
37
|
-
# @multi.fetch
|
44
|
+
# # Return un-paginated list of all items, optionally filtered
|
45
|
+
# @multi.fetch(since: nil)
|
38
46
|
# # => [Response] { user_id => [Array]<Event>, ... }
|
47
|
+
# # Options:
|
48
|
+
# # since: <timestamp> — if provided, returns all items posted since then
|
49
|
+
# # since: :unread — if provided, returns all unread items
|
39
50
|
#
|
40
51
|
# @multi.reset_last_read
|
41
52
|
# # => [Response] { user_id => [Time] last_read, ... }
|
@@ -48,7 +59,8 @@ module SimpleFeed
|
|
48
59
|
#
|
49
60
|
# @multi.last_read
|
50
61
|
# # => [Response] { user_id => [Time] last_read, ... }
|
51
|
-
#
|
62
|
+
#
|
63
|
+
#```
|
52
64
|
|
53
65
|
SimpleFeed::Providers.define_provider_methods(self) do |instance, method, *args, **opts, &block|
|
54
66
|
opts.merge!(user_ids: instance.user_ids)
|
@@ -16,39 +16,50 @@ module SimpleFeed
|
|
16
16
|
yield(user_id)
|
17
17
|
end
|
18
18
|
|
19
|
+
#```ruby
|
20
|
+
# @activity = SimpleFeed.get(:feed_name).activity(current_user.id)
|
19
21
|
#
|
20
|
-
#
|
22
|
+
# @activity.store(value:, at:)
|
23
|
+
# @activity.store(event:)
|
24
|
+
# # => [Boolean] true if the value was stored, false if it wasn't.
|
21
25
|
#
|
22
|
-
#
|
26
|
+
# @activity.delete(value:, at:)
|
27
|
+
# @activity.delete(event:)
|
28
|
+
# # => [Boolean] true if the value was removed, false if it didn't exist
|
23
29
|
#
|
24
|
-
#
|
25
|
-
#
|
30
|
+
# @activity.delete_if do |user_id, event|
|
31
|
+
# # if the block returns true, the event is deleted
|
32
|
+
# end
|
26
33
|
#
|
27
|
-
#
|
28
|
-
#
|
34
|
+
# @activity.wipe
|
35
|
+
# # => [Boolean] true if user activity was found and deleted, false otherwise
|
29
36
|
#
|
30
|
-
#
|
31
|
-
#
|
37
|
+
# @activity.paginate(page:, per_page:, peek: false, with_total: false)
|
38
|
+
# # => [Array]<Event>
|
39
|
+
# # Options:
|
40
|
+
# # peek: true — does not reset last_read, otherwise it does.
|
41
|
+
# # with_total: true — returns a hash for each user_id:
|
42
|
+
# # => { events: Array<Event>, total_count: 3 }
|
32
43
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
44
|
+
# # Return un-paginated list of all items, optionally filtered
|
45
|
+
# @activity.fetch(since: nil)
|
46
|
+
# # => [Array]<Event>
|
47
|
+
# # Options:
|
48
|
+
# # since: <timestamp> — if provided, returns all items posted since then
|
49
|
+
# # since: :unread — if provided, returns all unread items
|
36
50
|
#
|
37
|
-
#
|
38
|
-
#
|
51
|
+
# @activity.reset_last_read
|
52
|
+
# # => [Time] last_read
|
39
53
|
#
|
40
|
-
#
|
41
|
-
#
|
54
|
+
# @activity.total_count
|
55
|
+
# # => [Integer] total_count
|
42
56
|
#
|
43
|
-
#
|
44
|
-
#
|
57
|
+
# @activity.unread_count
|
58
|
+
# # => [Integer] unread_count
|
45
59
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
# @ua.last_read
|
50
|
-
# # => [Time] last_read
|
51
|
-
# ```
|
60
|
+
# @activity.last_read
|
61
|
+
# # => [Time] last_read
|
62
|
+
|
52
63
|
|
53
64
|
SimpleFeed::Providers.define_provider_methods(self) do |instance, method, *args, **opts, &block|
|
54
65
|
response = instance.user_activity.send(method, *args, **opts, &block)
|
data/lib/simplefeed/dsl.rb
CHANGED
@@ -11,10 +11,10 @@ module SimpleFeed
|
|
11
11
|
# include SimpleFeed::DSL
|
12
12
|
#
|
13
13
|
# with_activity(SimpleFeed.get(:newsfeed).activity(user_id)) do
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
14
|
+
# store(value: 'hello', at: Time.now) #=> true
|
15
|
+
# fetch # => [ Event, Event, ... ]
|
16
|
+
# total_count # => 12
|
17
|
+
# unread_count # => 4
|
18
18
|
# end
|
19
19
|
#
|
20
20
|
module DSL
|
@@ -3,16 +3,19 @@ require 'simplefeed/activity/single_user'
|
|
3
3
|
require 'simplefeed/activity/multi_user'
|
4
4
|
module SimpleFeed
|
5
5
|
module DSL
|
6
|
+
# This module exports method #color_dump which receives an activity and
|
7
|
+
# then prints out a report about the activity, including the event
|
8
|
+
# data found for a given user.
|
6
9
|
module Formatter
|
7
10
|
include SimpleFeed::DSL
|
8
11
|
|
9
12
|
attr_accessor :activity, :feed
|
10
13
|
|
11
|
-
def color_dump(
|
12
|
-
|
13
|
-
|
14
|
+
def color_dump(this_activity = activity)
|
15
|
+
this_activity = if this_activity.is_a?(SimpleFeed::Activity::SingleUser)
|
16
|
+
this_activity.feed.activity([this_activity.user_id])
|
14
17
|
else
|
15
|
-
|
18
|
+
this_activity
|
16
19
|
end
|
17
20
|
_puts
|
18
21
|
|
@@ -22,34 +25,34 @@ module SimpleFeed
|
|
22
25
|
field('Max Size', feed.max_size, "\n")
|
23
26
|
end
|
24
27
|
|
25
|
-
with_activity(
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
with_activity(this_activity) do
|
29
|
+
this_activity.each do |user_id|
|
30
|
+
this_last_event_at = nil
|
31
|
+
this_last_read = (last_read[user_id] || 0.0).to_f
|
29
32
|
|
30
33
|
[['User ID', user_id, "\n"],
|
31
34
|
['Activities', sprintf('%d total, %d unread', total_count[user_id], unread_count[user_id]), "\n"],
|
32
|
-
['Last Read',
|
35
|
+
['Last Read', this_last_read ? Time.at(this_last_read) : 'N/A'],
|
33
36
|
].each do |field, value, *args|
|
34
37
|
field(field, value, *args)
|
35
38
|
end
|
36
39
|
|
37
40
|
_puts; hr '¨'
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
this_events = fetch[user_id]
|
43
|
+
this_events_count = this_events.size
|
44
|
+
this_events.each_with_index do |_event, _index|
|
42
45
|
|
43
|
-
if
|
44
|
-
print_last_read_separator(
|
45
|
-
elsif
|
46
|
-
print_last_read_separator(
|
46
|
+
if this_last_event_at.nil? && _event.at < this_last_read
|
47
|
+
print_last_read_separator(this_last_read)
|
48
|
+
elsif this_last_event_at && this_last_read < this_last_event_at && this_last_read > _event.at
|
49
|
+
print_last_read_separator(this_last_read)
|
47
50
|
end
|
48
51
|
|
49
|
-
|
52
|
+
this_last_event_at = _event.at # float
|
50
53
|
_print "[%2d] %16s %s\n", _index, _event.time.strftime(TIME_FORMAT).blue.bold, _event.value
|
51
|
-
if _index ==
|
52
|
-
print_last_read_separator(
|
54
|
+
if _index == this_events_count - 1 && this_last_read < _event.at
|
55
|
+
print_last_read_separator(this_last_read)
|
53
56
|
end
|
54
57
|
end
|
55
58
|
end
|
@@ -97,13 +100,17 @@ module SimpleFeed
|
|
97
100
|
end
|
98
101
|
|
99
102
|
def hr(char = '—')
|
100
|
-
_print
|
103
|
+
_print(_hr(char).magenta)
|
104
|
+
end
|
105
|
+
|
106
|
+
def _hr(char = '—')
|
107
|
+
char * 75 + "\n"
|
101
108
|
end
|
102
109
|
|
103
110
|
def header(message = nil)
|
104
|
-
|
105
|
-
block_given? ? yield : _print(message.
|
106
|
-
|
111
|
+
_print(_hr.green.bold)
|
112
|
+
block_given? ? yield : _print(message.green.italic + "\n")
|
113
|
+
_print(_hr.green.bold)
|
107
114
|
end
|
108
115
|
end
|
109
116
|
end
|
data/lib/simplefeed/event.rb
CHANGED
@@ -8,12 +8,12 @@ module SimpleFeed
|
|
8
8
|
def initialize(*args, value: nil, at: Time.now)
|
9
9
|
if args && args.size > 0
|
10
10
|
self.value = args[0]
|
11
|
-
self.at = args[1]
|
12
|
-
else
|
13
|
-
self.value = value
|
14
|
-
self.at = at
|
11
|
+
self.at = args[1]
|
15
12
|
end
|
16
13
|
|
14
|
+
self.value ||= value
|
15
|
+
self.at ||= at
|
16
|
+
|
17
17
|
self.at = self.at.to_f
|
18
18
|
|
19
19
|
validate!
|
@@ -53,22 +53,21 @@ module SimpleFeed
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def to_s
|
56
|
-
"Event
|
56
|
+
"<SimpleFeed::Event: value='#{value}', at='#{at}', time='#{time}'>"
|
57
57
|
end
|
58
58
|
|
59
59
|
def to_color_s
|
60
60
|
counter = 0
|
61
|
-
to_s.
|
61
|
+
to_s.split(/[']/).map do |word|
|
62
62
|
counter += 1
|
63
|
-
counter.even? ? word.bold
|
64
|
-
end.join('')
|
63
|
+
counter.even? ? word.yellow.bold : word.blue
|
64
|
+
end.join('')
|
65
65
|
end
|
66
66
|
|
67
67
|
def inspect
|
68
68
|
super
|
69
69
|
end
|
70
70
|
|
71
|
-
|
72
71
|
private
|
73
72
|
|
74
73
|
def validate!
|
@@ -77,11 +76,5 @@ module SimpleFeed
|
|
77
76
|
end
|
78
77
|
end
|
79
78
|
|
80
|
-
def copy(&block)
|
81
|
-
copy = self.clone
|
82
|
-
copy.instance_eval(&block)
|
83
|
-
copy
|
84
|
-
end
|
85
|
-
|
86
79
|
end
|
87
80
|
end
|
@@ -1,7 +1,11 @@
|
|
1
1
|
require 'base62-rb'
|
2
2
|
require 'hashie'
|
3
3
|
require 'set'
|
4
|
-
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'knjrbfw'
|
7
|
+
rescue LoadError
|
8
|
+
end
|
5
9
|
|
6
10
|
require 'simplefeed/event'
|
7
11
|
require_relative 'paginator'
|
@@ -57,17 +61,26 @@ module SimpleFeed
|
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
60
|
-
def fetch(user_ids:)
|
64
|
+
def fetch(user_ids:, since: nil)
|
61
65
|
with_response_batched(user_ids) do |key|
|
62
|
-
|
66
|
+
if since == :unread
|
67
|
+
result = activity(key).reject { |event| event.at < user_record(key).last_read.to_f }
|
68
|
+
reset_last_read(user_ids: user_ids)
|
69
|
+
result
|
70
|
+
elsif since
|
71
|
+
activity(key).reject { |event| event.at < since.to_f }
|
72
|
+
else
|
73
|
+
activity(key)
|
74
|
+
end
|
63
75
|
end
|
64
76
|
end
|
65
77
|
|
66
|
-
def paginate(user_ids:, page:, per_page: feed.per_page,
|
67
|
-
reset_last_read(user_ids: user_ids) unless
|
78
|
+
def paginate(user_ids:, page:, per_page: feed.per_page, with_total: false, peek: false)
|
79
|
+
reset_last_read(user_ids: user_ids) unless peek
|
68
80
|
with_response_batched(user_ids) do |key|
|
69
81
|
activity = activity(key)
|
70
|
-
(page && page > 0) ? activity[((page - 1) * per_page)...(page * per_page)] : activity
|
82
|
+
result = (page && page > 0) ? activity[((page - 1) * per_page)...(page * per_page)] : activity
|
83
|
+
with_total ? { events: result, total_count: activity.length } : result
|
71
84
|
end
|
72
85
|
end
|
73
86
|
|
@@ -97,8 +110,12 @@ module SimpleFeed
|
|
97
110
|
end
|
98
111
|
|
99
112
|
def total_memory_bytes
|
100
|
-
|
101
|
-
|
113
|
+
if defined?(::Knj)
|
114
|
+
analyzer = Knj::Memory_analyzer::Object_size_counter.new(self.h)
|
115
|
+
analyzer.calculate_size
|
116
|
+
else
|
117
|
+
raise LoadError, 'Please run "gem install knjrbfw" to get accurate hash size'
|
118
|
+
end
|
102
119
|
end
|
103
120
|
|
104
121
|
def total_users
|
@@ -15,6 +15,14 @@ module SimpleFeed
|
|
15
15
|
self.debug
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.with_debug(&block)
|
19
|
+
previous_value = SimpleFeed::Providers::Redis.debug
|
20
|
+
SimpleFeed::Providers::Redis.debug = true
|
21
|
+
result = yield if block_given?
|
22
|
+
SimpleFeed::Providers::Redis.debug = previous_value
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
18
26
|
class << self
|
19
27
|
attr_accessor :debug
|
20
28
|
end
|
@@ -24,10 +32,32 @@ module SimpleFeed
|
|
24
32
|
end
|
25
33
|
|
26
34
|
class LoggingRedis < Struct.new(:redis)
|
35
|
+
@stream = STDOUT
|
36
|
+
@disable_color = false
|
37
|
+
class << self
|
38
|
+
# in case someone might prefer to dump it into STDOUT instead, just set
|
39
|
+
# SimpleFeed::Providers::Redis::Driver::LoggingRedis.stream = STDOUT | STDERR | etc...
|
40
|
+
attr_accessor :stream, :disable_color
|
41
|
+
end
|
42
|
+
|
27
43
|
def method_missing(m, *args, &block)
|
28
44
|
if redis.respond_to?(m)
|
29
|
-
|
30
|
-
|
45
|
+
t1 = Time.now
|
46
|
+
result = redis.send(m, *args, &block)
|
47
|
+
delta = Time.now - t1
|
48
|
+
colors = [:blue, nil, :blue, :blue, :yellow, :cyan, nil, :blue]
|
49
|
+
components = [
|
50
|
+
Time.now.strftime('%H:%M:%S.%L'), ' rtt=',
|
51
|
+
(sprintf '%.5f', delta*1000), ' ms ',
|
52
|
+
(sprintf '%15s ', m.to_s.upcase),
|
53
|
+
(sprintf '%-40s', args.inspect.gsub(/[",\[\]]/, '')), ' ⇒ ',
|
54
|
+
(result.is_a?(::Redis::Future) ? '' : result.to_s)]
|
55
|
+
components.each_with_index do |component, index|
|
56
|
+
color = self.class.disable_color ? nil : colors[index]
|
57
|
+
component = component.send(color) if color
|
58
|
+
self.class.stream.printf component
|
59
|
+
end
|
60
|
+
self.class.stream.puts
|
31
61
|
result
|
32
62
|
else
|
33
63
|
super
|
@@ -75,10 +75,21 @@ module SimpleFeed
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
-
|
79
|
-
|
78
|
+
def fetch(user_ids:, since: nil)
|
79
|
+
if since == :unread
|
80
|
+
last_read_response = with_response_pipelined(user_ids) do |redis, key|
|
81
|
+
get_users_last_read(redis, key)
|
82
|
+
end
|
83
|
+
reset_last_read(user_ids: user_ids)
|
84
|
+
end
|
80
85
|
with_response_pipelined(user_ids) do |redis, key|
|
81
|
-
|
86
|
+
if since == :unread
|
87
|
+
redis.zrevrangebyscore(key.data, '+inf', (last_read_response.delete(key.user_id) || 0).to_f, withscores: true)
|
88
|
+
elsif since
|
89
|
+
redis.zrevrangebyscore(key.data, '+inf', since.to_f, withscores: true)
|
90
|
+
else
|
91
|
+
redis.zrevrange(key.data, 0, -1, withscores: true)
|
92
|
+
end
|
82
93
|
end
|
83
94
|
end
|
84
95
|
|
data/lib/simplefeed/version.rb
CHANGED
Binary file
|
Binary file
|
Binary file
|
data/simple-feed.gemspec
CHANGED
@@ -29,14 +29,12 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_dependency 'connection_pool', '~> 2'
|
30
30
|
spec.add_dependency 'activesupport'
|
31
31
|
spec.add_dependency 'colored2'
|
32
|
-
spec.add_dependency 'knjrbfw'
|
33
32
|
|
34
33
|
spec.add_development_dependency 'awesome_print'
|
35
34
|
spec.add_development_dependency 'yard'
|
36
35
|
spec.add_development_dependency 'simplecov', '~> 0.12'
|
37
36
|
spec.add_development_dependency 'codeclimate-test-reporter', '~> 1.0'
|
38
|
-
|
39
|
-
spec.add_development_dependency 'ventable'
|
37
|
+
spec.add_development_dependency 'knjrbfw'
|
40
38
|
spec.add_development_dependency 'bundler'
|
41
39
|
spec.add_development_dependency 'rake', '~> 10.0'
|
42
40
|
spec.add_development_dependency 'rspec', '~> 3.5'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple-feed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Gredeskoul
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-12-
|
11
|
+
date: 2016-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base62-rb
|
@@ -108,20 +108,6 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: knjrbfw
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :runtime
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
111
|
- !ruby/object:Gem::Dependency
|
126
112
|
name: awesome_print
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -179,7 +165,7 @@ dependencies:
|
|
179
165
|
- !ruby/object:Gem::Version
|
180
166
|
version: '1.0'
|
181
167
|
- !ruby/object:Gem::Dependency
|
182
|
-
name:
|
168
|
+
name: knjrbfw
|
183
169
|
requirement: !ruby/object:Gem::Requirement
|
184
170
|
requirements:
|
185
171
|
- - ">="
|
@@ -301,8 +287,9 @@ files:
|
|
301
287
|
- lib/simplefeed/providers/serialization/key.rb
|
302
288
|
- lib/simplefeed/response.rb
|
303
289
|
- lib/simplefeed/version.rb
|
304
|
-
- man/
|
305
|
-
- man/
|
290
|
+
- man/activity-feed-action.png
|
291
|
+
- man/running-example-redis-debug.png
|
292
|
+
- man/running-example.png
|
306
293
|
- simple-feed.gemspec
|
307
294
|
homepage: https://github.com/kigster/simple-feed
|
308
295
|
licenses:
|
data/man/running-the-example.png
DELETED
Binary file
|
data/man/sf-example.png
DELETED
Binary file
|