simple-feed 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
cache: bundler
|
2
|
+
rvm:
|
3
|
+
- 2.3.1
|
4
|
+
- 2.4.0-preview2
|
5
|
+
services:
|
6
|
+
- redis-server
|
7
|
+
before_script:
|
8
|
+
script: bundle exec rspec
|
9
|
+
after_success:
|
10
|
+
- bundle exec codeclimate-test-reporter
|
11
|
+
env:
|
12
|
+
notifications:
|
13
|
+
slack:
|
14
|
+
secure: QaaD9WpCYoGFITVd8ku3cjkw1ADJ/rqsPgFGsbRxjPOj1HDzkkUVs20IK0AysxeI6CPY31rfWqbBcE4I4dUE49uIhpOoBXZ8GDYunUONeEmqGwrqZtV/8NqMLNc0Ouu5Jp/KVlnMO93Pcg97BBW81t1koxqyvgSU5oEmJdTmQZIsRNprhczeCj7T6jbb12LEA8uKi2qnp+FAhL5NJczHxqWCd2pWThtbA+hiRmH/mU0480n1eRForN51jDYUrgxb4PamhfUUG1xkzMPzGsvJTMCQDOG4eyHaq71vlZ3z9BbQG6vbcjfEQHVwfrPRz1k0luIlxMXJxszCFnnLVqeRYs85y4FOQSrLsDfsyLCU+1NgLqtseN6jNKOtY624Ok7YvYzsVYez/CPWUG1d2NIRMYQ+BoL2VQ3SDfvr4bYQ02QmqlhM1bKqmBoM+WmiH1FQk6CKOFcmlSkgkFilLi6YSB/EisQ5V5jo7DnzD38VVACo6J6SgVk2D6soONE3zev34Qa6PIOuwTTlXl6JKH0cpiG058lI+Oza+0xi0jg58sZG3jxeGM7m4wg66htgyN1oRQjIukSR+IJ3PZlFKTLvx2rCUcYz+8fRpPCMTSIVLeju5oPqOm0VRUni2dG5JGyNACV8EhI4ZMcfl7+uFTIagCnllRftBx8lY6AxWN3WpDE=
|
data/.yardopts
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'guard/rspec'
|
3
|
+
|
4
|
+
guard :rspec,
|
5
|
+
version: 3,
|
6
|
+
cmd: 'bundle exec rspec',
|
7
|
+
bundler: true,
|
8
|
+
all_after_pass: false,
|
9
|
+
all_on_start: false,
|
10
|
+
keep_failed: false do
|
11
|
+
|
12
|
+
watch(%r{.*\.gemspec}) { 'spec' }
|
13
|
+
watch(%r{^lib/(.+)\.rb$}) { 'spec' }
|
14
|
+
watch(%r{^spec/.+_spec\.rb$})
|
15
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
16
|
+
watch(%r{spec/support/.*}) { 'spec' }
|
17
|
+
end
|
18
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Konstantin Gredeskoul
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,457 @@
|
|
1
|
+
# SimpleFeed — Scalable, easy to use activity feed implementation.
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/simple-feed.svg)](https://badge.fury.io/rb/simple-feed)
|
4
|
+
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kigster/simple-feed/master/LICENSE.txt)
|
5
|
+
[![Build Status](https://travis-ci.org/kigster/simple-feed.svg?branch=master)](https://travis-ci.org/kigster/simple-feed)
|
6
|
+
[![Code Climate](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/gpa.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
|
7
|
+
[![Test Coverage](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/coverage.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/coverage)
|
8
|
+
[![Issue Count](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/issue_count.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
|
9
|
+
|
10
|
+
This is a ruby implementation of a fast simple feed commonly used in a typical social network-like applications. The implementation is optimized for **read-time performance** and high concurrency (lots of users). A default Redis-based provider implementation is provided, with the API supporting new providers very easily.
|
11
|
+
|
12
|
+
<div style="border: 2px solid #222; padding: 10px; background: #f5f5f5; font-family: 'HelveticaNeue-CondensedBold'; font-size: 14pt;">
|
13
|
+
<ol>
|
14
|
+
<li>Please note that this project is under <em>active development</em>, and is not yet completed.<br/></li>
|
15
|
+
<li>We thank <em><a href="http://simbi.com">Simbi, Inc.</a></em> for sponsoring the development of this open source library.</li>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
## What is an activity feed?
|
19
|
+
|
20
|
+
> Activity feed is a visual representation of a time-ordered, reverse chronological list of events which can be:
|
21
|
+
>
|
22
|
+
> * 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
|
+
> * aggregated across several actors for a similar event type, eg. "John, Mary, etc.. followed George"
|
28
|
+
|
29
|
+
Here is an example of a text-based simple feed that is very common today on social networking sites.
|
30
|
+
|
31
|
+
[![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-example.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-example.png)
|
32
|
+
|
33
|
+
The _stories_ in the feed depend entirely on the application using this
|
34
|
+
library, therefore to integrate with SimpleFeed requires implementing
|
35
|
+
several _glue points_ in your code.
|
36
|
+
|
37
|
+
## Challenges
|
38
|
+
|
39
|
+
Activity feeds tend to be challenging due to the large number of event types that it typically includes, and the requirement for it to scale to massive numbers of concurrent users. Therefore common implementations tend to focus on either:
|
40
|
+
|
41
|
+
* optimizing the read time performance by pre-computing the feed for each user ahead of time
|
42
|
+
* OR optimizing the various ranking algorithms by computing the feed at read time, with complex forms of caching addressing the performance requirements.
|
43
|
+
|
44
|
+
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
|
+
|
46
|
+
## Overview
|
47
|
+
|
48
|
+
The feed library aims to address the following goals:
|
49
|
+
|
50
|
+
* To define a minimalistic API for a typical event-based simple feed,
|
51
|
+
without tying it to any concrete provider implementation
|
52
|
+
* To make it easy to implement and plug in a new type of provider,
|
53
|
+
eg.using Couchbase or MongoDB
|
54
|
+
* To provide a scalable default provider implementation using Redis, which can support millions of users via sharding
|
55
|
+
* To support multiple simple feeds within the same application, but used for different purposes, eg. simple feed of my followers, versus simple feed of my own actions.
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
First you need to configure the Feed with a valid provider
|
60
|
+
implementation and a name.
|
61
|
+
|
62
|
+
### Configuration
|
63
|
+
|
64
|
+
Below we configure a feed called `:newsfeed`, which presumably
|
65
|
+
will be populated with the events coming from the followers.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
require 'simplefeed'
|
69
|
+
require 'simplefeed/providers/redis'
|
70
|
+
|
71
|
+
# Let's define a Redis-based feed, and wrap Redis in a in a ConnectionPool.
|
72
|
+
SimpleFeed.define(:newsfeed) do |f|
|
73
|
+
f.provider = SimpleFeed.provider(:redis,
|
74
|
+
redis: -> { ::Redis.new },
|
75
|
+
pool_size: 10)
|
76
|
+
f.per_page = 50
|
77
|
+
f.batch_size = 10 # default batch size
|
78
|
+
f.namespace = 'nf'
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
After the feed is defined, the gem creates a similarly named method
|
83
|
+
under the `SimpleFeed` namespace to access the feed. For example, given
|
84
|
+
a name such as `:newsfeed` the following are all valid ways of
|
85
|
+
accessing the feed:
|
86
|
+
|
87
|
+
* `SimpleFeed.newsfeed`
|
88
|
+
* `SimpleFeed.get(:newsfeed)`
|
89
|
+
|
90
|
+
You can also get a full list of currently defined feeds with `SimpleFeed.feed_names` method.
|
91
|
+
|
92
|
+
### Reading from and writing to the feed
|
93
|
+
|
94
|
+
For the impatient here is a quick way to get started with the
|
95
|
+
`SimpleFeed`.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
activity = SimpleFeed.get(:followers).activity(@current_user.id)
|
99
|
+
|
100
|
+
# Store directly the value and the optional time stamp
|
101
|
+
activity.store(value: 'hello')
|
102
|
+
# => true
|
103
|
+
|
104
|
+
# or equivalent:
|
105
|
+
@event = SimpleFeed::Event.new(value: 'hello', at: Time.now)
|
106
|
+
activity.store(event: @event)
|
107
|
+
# => false # false indicates that the same event is already in the feed.
|
108
|
+
```
|
109
|
+
|
110
|
+
As we've added events for this user, we can request them back, sorted by
|
111
|
+
the time and paginated. If you are using a distributed provider, such as
|
112
|
+
`Redis`, the events can be retrieved by any ruby process in your
|
113
|
+
application, not just the one that published the event (which is the
|
114
|
+
case for the "toy" `Hash::Provider`.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
activity.paginate(page: 1)
|
118
|
+
# => [ <SimpleFeed::Event#0x2134afa value='hello' at='2016-11-20 23:32:56 -0800'> ]
|
119
|
+
```
|
120
|
+
|
121
|
+
### The Two Forms of the API
|
122
|
+
|
123
|
+
The feed API is offered in a single-user and a batch (multi-user) forms.
|
124
|
+
|
125
|
+
The only and primary difference is in what the methods return. In the
|
126
|
+
single user case, the return of, say, `#total_count` is an `Integer`
|
127
|
+
value representing the total count for this user.
|
128
|
+
|
129
|
+
In the multi-user case, the return is a `SimpleFeed::Response` instance,
|
130
|
+
that can be thought of as a `Hash`, that has the user IDs as the keys,
|
131
|
+
and return results for each user as a value.
|
132
|
+
|
133
|
+
Please see further below the details about the [Batch API](#bach-api).
|
134
|
+
|
135
|
+
<a name="single-user-api"/>
|
136
|
+
|
137
|
+
##### Single-User API
|
138
|
+
|
139
|
+
This API should be used typically for _read_ operations, and is accessed
|
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
|
+
|
144
|
+
Below is a user session that demonstrates simple return values from the
|
145
|
+
Feed operations for a given user:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
require 'simplefeed'
|
149
|
+
|
150
|
+
# Define the feed using an in-memory Hash provider, which uses
|
151
|
+
# SortedSet to keep user's events sorted.
|
152
|
+
SimpleFeed.define(:notifications) do |f|
|
153
|
+
f.provider = SimpleFeed.provider(:hash)
|
154
|
+
f.per_page = 50
|
155
|
+
f.per_page = 2
|
156
|
+
end
|
157
|
+
|
158
|
+
# Let's get the Activity instance that wraps this user_id
|
159
|
+
activity = SimpleFeed.get(:notifications).activity(user_id)
|
160
|
+
# => [... complex object removed for brevity ]
|
161
|
+
|
162
|
+
# let's clear out this feed to ensure it's empty
|
163
|
+
activity.wipe
|
164
|
+
# => true
|
165
|
+
|
166
|
+
# Let's verify that the counts for this feed are at zero
|
167
|
+
activity.total_count
|
168
|
+
#=> 0
|
169
|
+
|
170
|
+
activity.unread_count
|
171
|
+
#=> 0
|
172
|
+
|
173
|
+
# Store some events
|
174
|
+
activity.store(value: 'hello')
|
175
|
+
activity.store(value: 'goodbye')
|
176
|
+
|
177
|
+
# Now we can paginate the events, which by default resets "last_read" timestamp the user
|
178
|
+
activity.paginate(page: 1)
|
179
|
+
# [
|
180
|
+
# [0] #<SimpleFeed::Event#70138821650220 {"value":"goodbye","at":1480475294.0579991}>,
|
181
|
+
# [1] #<SimpleFeed::Event#70138821649420 {"value":"hello","at":1480475294.057138}>
|
182
|
+
# ]
|
183
|
+
|
184
|
+
# Now the unread_count should return 0 since the user just "viewed" the feed.
|
185
|
+
activity.unread_count
|
186
|
+
#=> 0
|
187
|
+
```
|
188
|
+
|
189
|
+
You can fetch all items in the feed using `#fetch`, and you can
|
190
|
+
`#paginate` without resetting the `last_read` timestamp by passing the
|
191
|
+
`peek: true` as a parameter.
|
192
|
+
|
193
|
+
<a name="batch-api"/>
|
194
|
+
|
195
|
+
##### Batch (Multi-User) API
|
196
|
+
|
197
|
+
This API should be used when dealing with an array of users (or, in the
|
198
|
+
future, a Proc or an ActiveRecord relation).
|
199
|
+
|
200
|
+
> There are several reasons why this API should be preferred for
|
201
|
+
> operations that perform a similar action across a range of users:
|
202
|
+
> _various provider implementations can be heavily optimized for
|
203
|
+
> concurrency, and performance_.
|
204
|
+
>
|
205
|
+
> The Redis Provider, for example, uses a notion of `pipelining` to send
|
206
|
+
> updates for different users asynchronously and concurrently.
|
207
|
+
|
208
|
+
Multi-user operations return a `SimpleFeed::Response` object, which can
|
209
|
+
be used as a hash (keyed on user_id) to fetch the result of a given
|
210
|
+
user.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
# Using the Feed API with, eg #find_in_batches
|
214
|
+
@event_producer.followers.find_in_batches do |group|
|
215
|
+
|
216
|
+
# Convert a group to the array of IDs and get ready to store
|
217
|
+
activity = SimpleFeed.get(:followers).activity(group.map(&:id))
|
218
|
+
activity.store(value: "#{@event_producer.name} liked an article")
|
219
|
+
|
220
|
+
# => [Response] { user_id1 => [Boolean], user_id2 => [Boolean]... }
|
221
|
+
# true if the value was stored, false if it wasn't.
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
##### DSL
|
226
|
+
|
227
|
+
The library offers a convenient DSL for adding feed functionality into
|
228
|
+
your current scope.
|
229
|
+
|
230
|
+
To use the module, just include `SimpleFeed::DSL` where needed, which
|
231
|
+
exports just one primary method `with_activity'. You call this method
|
232
|
+
and pass an activity object created for a set of users (or a single
|
233
|
+
user), like so:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
require 'simplefeed/dsl'
|
237
|
+
include SimpleFeed::DSL
|
238
|
+
|
239
|
+
feed = SimpleFeed.newsfeed
|
240
|
+
activity = feed.activity(current_user.id)
|
241
|
+
data_to_store = %w(France Germany England)
|
242
|
+
|
243
|
+
def report(value)
|
244
|
+
puts value
|
245
|
+
end
|
246
|
+
|
247
|
+
with_activity(activity, countries: data_to_store) do
|
248
|
+
# we can use countries as a variable because it was passed above in **opts
|
249
|
+
countries.each do |country|
|
250
|
+
# we can call #store without a receiver because the block is passed to
|
251
|
+
# instance_eval
|
252
|
+
store(value: country) { |result| report(result ? 'success' : 'failure') }
|
253
|
+
# we can call #report inside the proc because it is evaluated in the
|
254
|
+
# outside context of the #with_activity
|
255
|
+
end
|
256
|
+
printf "Activity counts are: %d unread of %d total\n", unread_count, total_count
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
<a name="api"/>
|
261
|
+
|
262
|
+
## Complete API
|
263
|
+
|
264
|
+
### Single User
|
265
|
+
|
266
|
+
For a single user, via the instance of
|
267
|
+
`SimpleFeed::Activity::UserActivity` class:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
require 'simplefeed'
|
271
|
+
|
272
|
+
@ua = SimpleFeed.get(:news).activity(current_user.id)
|
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
|
314
|
+
|
315
|
+
Each API call at this level expects an array of user IDs, therefore the
|
316
|
+
return value is an object, `SimpleFeed::Response`, containing individual
|
317
|
+
responses for each user, accessible via `response[user_id]` method.
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
@multi = SimpleFeed.get(:feed_name).activity(User.active.map(&:id))
|
321
|
+
|
322
|
+
@multi.store(value:, at:)
|
323
|
+
@multi.store(event:)
|
324
|
+
# => [Response] { user_id => [Boolean], ... } true if the value was stored, false if it wasn't.
|
325
|
+
|
326
|
+
@multi.delete(value:, at:)
|
327
|
+
@multi.delete(event:)
|
328
|
+
# => [Response] { user_id => [Boolean], ... } true if the value was removed, false if it didn't exist
|
329
|
+
|
330
|
+
@multi.delete_if do |user_id, event|
|
331
|
+
# if the block returns true, the event is deleted
|
332
|
+
end
|
333
|
+
|
334
|
+
@multi.wipe
|
335
|
+
# => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
|
336
|
+
|
337
|
+
@multi.paginate(page:, per_page:, peek: false)
|
338
|
+
# => [Response] { user_id => [Array]<Event>, ... }
|
339
|
+
# With (peak: true) does not reset last_read, otherwise it does.
|
340
|
+
|
341
|
+
@multi.fetch
|
342
|
+
# => [Response] { user_id => [Array]<Event>, ... }
|
343
|
+
|
344
|
+
@multi.reset_last_read
|
345
|
+
# => [Response] { user_id => [Time] last_read, ... }
|
346
|
+
|
347
|
+
@multi.total_count
|
348
|
+
# => [Response] { user_id => [Integer] total_count, ... }
|
349
|
+
|
350
|
+
@multi.unread_count
|
351
|
+
# => [Response] { user_id => [Integer] unread_count, ... }
|
352
|
+
|
353
|
+
@multi.last_read
|
354
|
+
# => [Response] { user_id => [Time] last_read, ... }
|
355
|
+
|
356
|
+
```
|
357
|
+
|
358
|
+
## Providers
|
359
|
+
|
360
|
+
A provider is an underlying implementation that persists the events for each user, together with some meta-data for each feed.
|
361
|
+
|
362
|
+
It is the intention of this gem that:
|
363
|
+
|
364
|
+
* it should be easy to swap providers
|
365
|
+
* it should be easy to add new providers
|
366
|
+
|
367
|
+
Each provider must implement exactly the public API of a provider shown
|
368
|
+
above (the `Feed` version, that receives `user_ids:` as arguments).
|
369
|
+
|
370
|
+
Two providers are available with this gem:
|
371
|
+
|
372
|
+
* `SimpleFeed::Providers::Redis::Provider` is the production-ready provider that uses the [sorted set Redis data type](https://redislabs.com/ebook/redis-in-action/part-2-core-concepts-2/chapter-3-commands-in-redis/3-5-sorted-sets) and their operations operations to store the events, scored by their time typically (but not necessarily). This provider is highly optimized for massive writes and can be sharded by using a _Twemproxy_ backend, and many small Redis shards.
|
373
|
+
|
374
|
+
* `SimpleFeed::Providers::HashProvider` is a pure Hash-like implementation of a provider that can be useful in unit tests of a host application. This provider could be used to write and read events within a single ruby process, can be serialized to and from a YAML file, and is therefore intended primarily for Feed emulations in automated tests.
|
375
|
+
|
376
|
+
|
377
|
+
### Redis Provider
|
378
|
+
|
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.
|
380
|
+
|
381
|
+
## Examples
|
382
|
+
|
383
|
+
Source code for the gem contains the `examples` folder with an example file that can be used to measure the performance of the Redis-based provider.
|
384
|
+
|
385
|
+
To run it, checkout the source of the library, and then:
|
386
|
+
|
387
|
+
```bash
|
388
|
+
git clone https://github.com/kigster/simple-feed.git
|
389
|
+
cd simple-feed
|
390
|
+
bundle
|
391
|
+
be rspec # make sure tests are passing
|
392
|
+
ruby examples/redis_provider_example.rb
|
393
|
+
```
|
394
|
+
|
395
|
+
The above command will help you download, setup all dependencies, and run the examples for a single user. To run examples for multiple users, just __just pass a number as a second argument__:, for example:
|
396
|
+
|
397
|
+
``` bash
|
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
|
+
```
|
405
|
+
|
406
|
+
Below is a an example output shown for a single user:
|
407
|
+
|
408
|
+
[![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-the-example.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-the-example.png)
|
409
|
+
|
410
|
+
### Generating Ruby API Documentation
|
411
|
+
|
412
|
+
```bash
|
413
|
+
rake doc
|
414
|
+
```
|
415
|
+
|
416
|
+
This should use Yard to generate the documentation, and open your browser once it's finished.
|
417
|
+
|
418
|
+
### Installation
|
419
|
+
|
420
|
+
Add this line to your application's Gemfile:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
gem 'simple-feed'
|
424
|
+
```
|
425
|
+
|
426
|
+
And then execute:
|
427
|
+
|
428
|
+
```
|
429
|
+
$ bundle
|
430
|
+
```
|
431
|
+
|
432
|
+
Or install it yourself as:
|
433
|
+
|
434
|
+
```
|
435
|
+
$ gem install simple-feed
|
436
|
+
```
|
437
|
+
|
438
|
+
### Development
|
439
|
+
|
440
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
441
|
+
|
442
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
443
|
+
|
444
|
+
### Contributing
|
445
|
+
|
446
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/simple-feed
|
447
|
+
|
448
|
+
### License
|
449
|
+
|
450
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
451
|
+
|
452
|
+
### Acknowledgements
|
453
|
+
|
454
|
+
* This project is conceived and sponsored by [Simbi, Inc.](https://simbi.com).
|
455
|
+
* Author's personal experience at [Wanelo, Inc.](https://wanelo.com) has served as an inspiration.
|
456
|
+
|
457
|
+
|