simple-feed 2.0.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/rubocop.yml +33 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.relaxed-rubocop-2.4.yml +174 -0
- data/.rspec +1 -1
- data/.rubocop.yml +42 -1149
- data/.rubocop_todo.yml +134 -0
- data/.travis.yml +15 -8
- data/Gemfile +2 -0
- data/Guardfile +3 -3
- data/{README.md → README.adoc} +205 -140
- data/Rakefile +6 -7
- data/bin/console +1 -0
- data/examples/shared/provider_example.rb +10 -3
- data/lib/simple-feed.rb +2 -0
- data/lib/simple_feed.rb +2 -0
- data/lib/simplefeed.rb +9 -7
- data/lib/simplefeed/activity/base.rb +2 -0
- data/lib/simplefeed/activity/multi_user.rb +8 -6
- data/lib/simplefeed/activity/single_user.rb +5 -4
- data/lib/simplefeed/dsl.rb +3 -1
- data/lib/simplefeed/dsl/activities.rb +4 -3
- data/lib/simplefeed/dsl/formatter.rb +12 -12
- data/lib/simplefeed/event.rb +4 -3
- data/lib/simplefeed/feed.rb +19 -22
- data/lib/simplefeed/key/template.rb +5 -9
- data/lib/simplefeed/key/type.rb +4 -2
- data/lib/simplefeed/providers.rb +24 -9
- data/lib/simplefeed/providers/base/provider.rb +6 -3
- data/lib/simplefeed/providers/hash.rb +2 -0
- data/lib/simplefeed/providers/hash/paginator.rb +4 -2
- data/lib/simplefeed/providers/hash/provider.rb +11 -21
- data/lib/simplefeed/providers/key.rb +20 -11
- data/lib/simplefeed/providers/proxy.rb +13 -12
- data/lib/simplefeed/providers/redis.rb +2 -0
- data/lib/simplefeed/providers/redis/driver.rb +23 -23
- data/lib/simplefeed/providers/redis/provider.rb +35 -33
- data/lib/simplefeed/providers/redis/stats.rb +12 -13
- data/lib/simplefeed/response.rb +4 -2
- data/lib/simplefeed/version.rb +3 -1
- data/simple-feed.gemspec +16 -14
- metadata +58 -41
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2020-05-23 01:49:13 -0700 using RuboCop version 0.83.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 1
|
10
|
+
# Cop supports --auto-correct.
|
11
|
+
# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity.
|
12
|
+
# SupportedStylesAlignWith: keyword, variable, start_of_line
|
13
|
+
Layout/EndAlignment:
|
14
|
+
Exclude:
|
15
|
+
- 'lib/simplefeed/dsl/formatter.rb'
|
16
|
+
|
17
|
+
# Offense count: 2
|
18
|
+
Lint/ShadowingOuterLocalVariable:
|
19
|
+
Exclude:
|
20
|
+
- 'lib/simplefeed/providers/redis/provider.rb'
|
21
|
+
|
22
|
+
# Offense count: 1
|
23
|
+
# Configuration parameters: AllowComments.
|
24
|
+
Lint/SuppressedException:
|
25
|
+
Exclude:
|
26
|
+
- 'lib/simplefeed/providers/hash/provider.rb'
|
27
|
+
|
28
|
+
# Offense count: 3
|
29
|
+
# Configuration parameters: AllowKeywordBlockArguments.
|
30
|
+
Lint/UnderscorePrefixedVariableName:
|
31
|
+
Exclude:
|
32
|
+
- 'lib/simplefeed/dsl/formatter.rb'
|
33
|
+
- 'lib/simplefeed/providers/redis/provider.rb'
|
34
|
+
|
35
|
+
# Offense count: 1
|
36
|
+
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
37
|
+
Lint/Void:
|
38
|
+
Exclude:
|
39
|
+
- 'lib/simplefeed/feed.rb'
|
40
|
+
|
41
|
+
# Offense count: 25
|
42
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
43
|
+
# ExcludedMethods: refine
|
44
|
+
Metrics/BlockLength:
|
45
|
+
Max: 185
|
46
|
+
|
47
|
+
# Offense count: 1
|
48
|
+
# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.
|
49
|
+
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
|
50
|
+
Naming/FileName:
|
51
|
+
Exclude:
|
52
|
+
- 'lib/simple-feed.rb'
|
53
|
+
|
54
|
+
# Offense count: 1
|
55
|
+
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
56
|
+
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
57
|
+
Naming/MemoizedInstanceVariableName:
|
58
|
+
Exclude:
|
59
|
+
- 'lib/simplefeed/providers/redis/stats.rb'
|
60
|
+
|
61
|
+
# Offense count: 5
|
62
|
+
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
63
|
+
# AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp
|
64
|
+
Naming/MethodParameterName:
|
65
|
+
Exclude:
|
66
|
+
- 'lib/simplefeed/dsl/formatter.rb'
|
67
|
+
- 'lib/simplefeed/providers/redis/driver.rb'
|
68
|
+
- 'spec/support/mock_feed.rb'
|
69
|
+
- 'spec/support/shared_examples_for_providers.rb'
|
70
|
+
|
71
|
+
# Offense count: 1
|
72
|
+
# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros.
|
73
|
+
# NamePrefix: is_, has_, have_
|
74
|
+
# ForbiddenPrefixes: is_, has_, have_
|
75
|
+
# AllowedMethods: is_a?
|
76
|
+
# MethodDefinitionMacros: define_method, define_singleton_method
|
77
|
+
Naming/PredicateName:
|
78
|
+
Exclude:
|
79
|
+
- 'spec/**/*'
|
80
|
+
- 'lib/simplefeed/response.rb'
|
81
|
+
|
82
|
+
# Offense count: 9
|
83
|
+
# Configuration parameters: EnforcedStyle.
|
84
|
+
# SupportedStyles: annotated, template, unannotated
|
85
|
+
Style/FormatStringToken:
|
86
|
+
Exclude:
|
87
|
+
- 'lib/simplefeed/dsl/formatter.rb'
|
88
|
+
- 'lib/simplefeed/providers/redis/driver.rb'
|
89
|
+
|
90
|
+
# Offense count: 3
|
91
|
+
# Configuration parameters: MinBodyLength.
|
92
|
+
Style/GuardClause:
|
93
|
+
Exclude:
|
94
|
+
- 'lib/simplefeed/event.rb'
|
95
|
+
- 'lib/simplefeed/providers/base/provider.rb'
|
96
|
+
- 'lib/simplefeed/providers/hash/provider.rb'
|
97
|
+
|
98
|
+
# Offense count: 1
|
99
|
+
# Configuration parameters: AllowIfModifier.
|
100
|
+
Style/IfInsideElse:
|
101
|
+
Exclude:
|
102
|
+
- 'lib/simplefeed/response.rb'
|
103
|
+
|
104
|
+
# Offense count: 2
|
105
|
+
Style/MethodMissingSuper:
|
106
|
+
Exclude:
|
107
|
+
- 'lib/simplefeed/providers/proxy.rb'
|
108
|
+
- 'lib/simplefeed/providers/redis/driver.rb'
|
109
|
+
|
110
|
+
# Offense count: 4
|
111
|
+
Style/MissingRespondToMissing:
|
112
|
+
Exclude:
|
113
|
+
- 'lib/simplefeed.rb'
|
114
|
+
- 'lib/simplefeed/providers/proxy.rb'
|
115
|
+
- 'lib/simplefeed/providers/redis/driver.rb'
|
116
|
+
|
117
|
+
# Offense count: 3
|
118
|
+
Style/MultilineTernaryOperator:
|
119
|
+
Exclude:
|
120
|
+
- 'lib/simplefeed/feed.rb'
|
121
|
+
- 'lib/simplefeed/providers/redis/provider.rb'
|
122
|
+
- 'lib/simplefeed/response.rb'
|
123
|
+
|
124
|
+
# Offense count: 1
|
125
|
+
Style/OptionalArguments:
|
126
|
+
Exclude:
|
127
|
+
- 'lib/simplefeed/providers/redis/provider.rb'
|
128
|
+
|
129
|
+
# Offense count: 3
|
130
|
+
Style/StructInheritance:
|
131
|
+
Exclude:
|
132
|
+
- 'lib/simplefeed/key/template.rb'
|
133
|
+
- 'lib/simplefeed/key/type.rb'
|
134
|
+
- 'lib/simplefeed/providers/redis/driver.rb'
|
data/.travis.yml
CHANGED
@@ -1,14 +1,21 @@
|
|
1
1
|
cache: bundler
|
2
2
|
rvm:
|
3
|
-
- 2.3.
|
4
|
-
- 2.4.
|
3
|
+
- 2.3.8
|
4
|
+
- 2.4.10
|
5
|
+
- 2.5.3
|
6
|
+
- 2.6.2
|
7
|
+
- 2.7.1
|
5
8
|
services:
|
6
9
|
- redis-server
|
7
10
|
before_script:
|
8
|
-
|
9
|
-
|
10
|
-
-
|
11
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
12
|
+
- chmod +x ./cc-test-reporter
|
13
|
+
- ./cc-test-reporter before-build
|
14
|
+
script: bundle exec rspec --format=documentation
|
15
|
+
after_script:
|
16
|
+
- bash <(curl -s https://codecov.io/bash)
|
17
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
11
18
|
env:
|
12
|
-
|
13
|
-
|
14
|
-
secure:
|
19
|
+
global:
|
20
|
+
CODECOV_TOKEN="2085c087-f833-49c7-a105-703bce882653"
|
21
|
+
secure: cdUlrz0XNCu0HB5lNLYZBF6yaukIkDZRlVsAbeeaNhPKCVj7MOvuGEUZ9TzQP0yXRaGXHy3xXem0MdDbnli5WYvHxj2MTrRVEDBiRm4jaZFHe3Rewr2mWmLqK1c4g9gvOTgjPNPTesdft87+wQuPedQLpfMGuedA86tkKzA2Tkc4J69DI93C02h6H89iiQFHL9ISNk5DzZh3FJo+fyi0JQlD6CAzWrVplCqf088qGdAk3Hy77Q2GKkVEdOb/P0cdmDLnFjekdh4b0TPI/uaqgu4cZeoebWQgg61K/enGLqxY2Lp0f4DsXQev+06d14KxIJMYHLuDgvlVwYMDInxQU7kXgTRgAnaxRdm0S7NbP89uBVkfIQiNGan7Qkvw2ayGhYCcu7Vgvjo6C2btZWLpxT0XvpQGfpvHrRrPnzFbFLLtG4rhJd3iiTbSa5LPDEi4Qx9uwGNy4iR6IB69R6dPwAR8P45XZE4JnJVUvrM/X9v9Yv2hAxnuCb48whalIozGqpwavLdYrgCf2pQecls74uw4mCxvDAg0EoskrDYuk4DYeZ507ajor4zX1avLUVfYk/0igpp6KeLENY5ozl1zsEvRcOldfdV8R9tKEGxliIgffe67Hc5/ZP+C/53lcK0nIxXYrB1I7Q+CVlEDNutNQNZRDfrNBYJVe/+RvmmdD+w=
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require 'guard/rspec'
|
3
5
|
|
4
6
|
guard :rspec,
|
@@ -8,11 +10,9 @@ guard :rspec,
|
|
8
10
|
all_after_pass: false,
|
9
11
|
all_on_start: false,
|
10
12
|
keep_failed: false do
|
11
|
-
|
12
|
-
watch(%r{.*\.gemspec}) { 'spec' }
|
13
|
+
watch(/.*\.gemspec/) { 'spec' }
|
13
14
|
watch(%r{^lib/(.+)\.rb$}) { 'spec' }
|
14
15
|
watch(%r{^spec/.+_spec\.rb$})
|
15
16
|
watch('spec/spec_helper.rb') { 'spec' }
|
16
17
|
watch(%r{spec/support/.*}) { 'spec' }
|
17
18
|
end
|
18
|
-
|
data/{README.md → README.adoc}
RENAMED
@@ -1,83 +1,137 @@
|
|
1
|
-
|
1
|
+
= SimpleFeed
|
2
|
+
:doctype: book
|
3
|
+
:toc:
|
4
|
+
:toclevels: 5
|
5
|
+
:sectnums:
|
2
6
|
|
7
|
+
== Scalable, Easy to Use Activity Feed Implementation.
|
3
8
|
|
4
|
-
|
5
|
-
|
9
|
+
image:https://img.shields.io/gem/v/simple-feed.svg[Gem Version,link=https://rubygems.org/gems/simple-feed]
|
10
|
+
image:https://img.shields.io/badge/license-MIT-blue.svg[MIT licensed,link=https://github.com/kigster/simple-feed/master/LICENSE.txt]
|
6
11
|
|
7
|
-
|
8
|
-
|
9
|
-
[![Test Coverage](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/coverage.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/coverage)
|
10
|
-
[![Issue Count](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/issue_count.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
|
11
|
-
[![Inline docs](http://inch-ci.org/github/kigster/simple-feed.svg?branch=master)](http://inch-ci.org/github/kigster/simple-feed)
|
12
|
-
[![Talk on Gitter](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/kigster/simple-feed)
|
12
|
+
image:https://github.com/kigster/simple-feed/workflows/Ruby/badge.svg?branch=master[Ruby,link=https://github.com/kigster/simple-feed/actions?query=workflow%3ARuby]
|
13
|
+
image:https://github.com/kigster/simple-feed/workflows/Rubocop/badge.svg?branch=master[Ruby,link=https://github.com/kigster/simple-feed/actions?query=workflow%3ARubocop]
|
13
14
|
|
14
|
-
|
15
|
+
image:https://travis-ci.org/kigster/simple-feed.svg?branch=master[Build Status,link=https://travis-ci.org/kigster/simple-feed]
|
16
|
+
image:https://api.codeclimate.com/v1/badges/a11061820895fcde635e/maintainability[Maintainability,link=https://codeclimate.com/github/kigster/simple-feed/maintainability]
|
17
|
+
image:https://api.codeclimate.com/v1/badges/a11061820895fcde635e/test_coverage[Test Coverage,link=https://codeclimate.com/github/kigster/simple-feed/test_coverage]
|
15
18
|
|
16
|
-
|
19
|
+
image:http://inch-ci.org/github/kigster/simple-feed.svg?branch=master[Inline docs,link=http://inch-ci.org/github/kigster/simple-feed]
|
20
|
+
image:https://img.shields.io/gitter/room/gitterHQ/gitter.svg[Talk on Gitter,link=https://gitter.im/kigster/simple-feed]
|
17
21
|
|
18
|
-
|
22
|
+
IMPORTANT: Please read the (somewhat outdated) blog post http://kig.re/2017/02/19/feeding-frenzy-with-simple-feed-activity-feed-ruby-gem.html[Feeding Frenzy with SimpleFeed] launching this library. Please leave comments or questions in the discussion thread at the bottom of that post. Thanks!
|
19
23
|
|
20
24
|
If you like to see this project grow, your donation of any amount is much appreciated.
|
21
25
|
|
22
|
-
|
26
|
+
image::https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif[Donate,link=https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FSFYYNEQ8RKWU]
|
27
|
+
'''
|
23
28
|
|
24
|
-
|
29
|
+
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 Ruby Hash-based provider.
|
25
30
|
|
26
|
-
|
31
|
+
*Important Notes and Acknowledgements:*
|
27
32
|
|
28
|
-
|
33
|
+
* SimpleFeed _does not depend on Ruby on Rails_ and is a *pure-ruby* implementation
|
34
|
+
* SimpleFeed requires ruby 2.3 or later
|
35
|
+
* SimpleFeed is currently live in production
|
36
|
+
* SimpleFeed is open source thanks to the generosity of *http://simbi.com[Simbi, Inc]*.
|
29
37
|
|
30
|
-
|
31
|
-
* SimpleFeed requires ruby 2.3 or later
|
32
|
-
* SimpleFeed is currently live in production
|
33
|
-
* SimpleFeed is open source thanks to the generosity of __[Simbi, Inc](http://simbi.com)__.
|
38
|
+
== Features
|
34
39
|
|
35
|
-
|
40
|
+
SimpleFeed offers the following features:
|
36
41
|
|
37
|
-
|
42
|
+
* Highly performant Redis-based activity feed
|
38
43
|
|
39
|
-
|
44
|
+
* Scales to millions of users (may need to use Twemproxy to shard across several Redis instances)
|
45
|
+
|
46
|
+
* Stores a fixed number of events per unique activity ID (eg, a user) — the default is 2000. When the full list is used, the oldest events are bumped off the feed (and are effectively trashed).
|
47
|
+
|
48
|
+
* Store user_id as either a string or an integer (the latter is base62 encoded when serialized, so very large users_ids are OK)
|
49
|
+
|
50
|
+
* Updating feeds of N users is roughly a O(N * log(N)) operation
|
51
|
+
|
52
|
+
* Reading feed for one user (or one type of user) is a O(1) operation
|
53
|
+
|
54
|
+
* For each activity (user) read the number of total and new items in the feed, computed since the user last saw their feed.
|
55
|
+
|
56
|
+
* Delete items from user's feed selectively (for instance, if a user unfollows someone they shouldn't see their events anymore).
|
57
|
+
|
58
|
+
* Automatically reset the timestamp when the user last read their feed when reading from the feed (or not, it's an argument). As soon as the user reads their feed (you call the `paginate` method), the "unread counter" is reset to 0.
|
59
|
+
|
60
|
+
* You can create as many different types of feeds per application as you like. No Singletons are used.
|
61
|
+
|
62
|
+
* Thread-safe implementation.
|
63
|
+
|
64
|
+
* Zero assumptions about what you are storing: the "data" is just a string. Serialize it with JSON, Marshall, YAML, or whatever.
|
65
|
+
|
66
|
+
== Background, Examples, Serialization, etc
|
67
|
+
|
68
|
+
Please read the additional documentation, including the examples, on the https://github.com/kigster/simple-feed/wiki[project's Github Wiki].
|
69
|
+
|
70
|
+
Below is a screen shot of an actual activity feed powered by this library.
|
71
|
+
|
72
|
+
image::https://raw.githubusercontent.com/kigster/simple-feed/master/man/activity-feed-action.png[usage]
|
73
|
+
|
74
|
+
== Usage
|
40
75
|
|
41
76
|
A key concept to understanding SimpleFeed gem, is that of a _provider_, which is effectively a persistence implementation for the events belonging to each user.
|
42
77
|
|
43
78
|
Two providers are supplied with this gem:
|
44
79
|
|
45
|
-
|
46
|
-
|
47
|
-
* The naïve `:hash` provider based on the ruby `Hash` class, that can be useful in unit tests, or in simple simulations.
|
80
|
+
* The production-ready `:redis` provider, which uses the https://redislabs.com/ebook/redis-in-action/part-2-core-concepts-2/chapter-3-commands-in-redis/3-5-sorted-sets[sorted set Redis data type] to store and fetch the events, scored by time (but not necessarily).
|
81
|
+
* The naïve `:hash` provider based on the ruby `Hash` class, that can be useful in unit tests, or in simple simulations.
|
48
82
|
|
49
83
|
You initialize a provider by using the `SimpleFeed.provider([Symbol])` method.
|
50
84
|
|
51
|
-
|
85
|
+
=== Configuration
|
52
86
|
|
53
|
-
Below we configure a feed called `:newsfeed`, which in this example will be populated with the various events coming from the followers.
|
87
|
+
Below we configure a feed called `:newsfeed`, which in this example will be populated with the various events coming from the followers.
|
54
88
|
|
55
|
-
|
89
|
+
[source,ruby]
|
90
|
+
----
|
56
91
|
require 'simplefeed'
|
57
92
|
|
58
|
-
# Let's define a Redis-based feed, and wrap Redis in a in a ConnectionPool.
|
93
|
+
# Let's define a Redis-based feed, and wrap Redis in a in a ConnectionPool.
|
59
94
|
SimpleFeed.define(:newsfeed) do |f|
|
60
|
-
f.provider = SimpleFeed.provider(:redis,
|
95
|
+
f.provider = SimpleFeed.provider(:redis,
|
61
96
|
redis: -> { ::Redis.new },
|
62
97
|
pool_size: 10)
|
63
98
|
f.per_page = 50 # default page size
|
64
99
|
f.batch_size = 10 # default batch size
|
65
100
|
f.namespace = 'nf' # only needed if you use the same redis for more than one feed
|
66
101
|
end
|
67
|
-
|
102
|
+
----
|
68
103
|
|
69
104
|
After the feed is defined, the gem creates a similarly named method under the `SimpleFeed` namespace to access the feed. For example, given a name such as `:newsfeed` the following are all valid ways of accessing the feed:
|
70
105
|
|
71
|
-
|
72
|
-
|
106
|
+
* `SimpleFeed.newsfeed`
|
107
|
+
* `SimpleFeed.get(:newsfeed)`
|
73
108
|
|
74
109
|
You can also get a full list of currently defined feeds with `SimpleFeed.feed_names` method.
|
75
110
|
|
76
|
-
|
111
|
+
=== User IDs
|
112
|
+
|
113
|
+
In the following section you will see the examples of reading and writing activity for users based on their ID.
|
114
|
+
|
115
|
+
SimpleFeed supports user IDs that are either numeric (integer) or string-based (eg, UUID).
|
116
|
+
|
117
|
+
If your User IDs are numeric, they generate redis keys using Base62 encoding (which makes them shorter, and more compact).
|
118
|
+
|
119
|
+
For string User IDs, the only transformation performed is the basic link:https://en.wikipedia.org/wiki/ROT13[`rot13`] in case user ids are semi-sensitive.
|
77
120
|
|
78
|
-
|
121
|
+
==== Partitioning Schema
|
79
122
|
|
80
|
-
|
123
|
+
You can take advantage of string user IDs for situations where your feed requires a composite keys for instance. Just remember that SimpleFeed does not care about what's in your user ID, and even what you call "a user". It's convenient to think of the activities in terms of users, because typically each user has a unique feed that only they see.
|
124
|
+
|
125
|
+
But you can just as easily use zip code as the unique activity ID, and create one feed of events per geographical location, that all folks living in that zip code share. But what about other countries?
|
126
|
+
|
127
|
+
Now you use partioning scheme: make "user_id" a combination `iso_country_code.postal_code`, eg for San Francisco, you'd use `us.94107`, but for Australia you could use, eg `au.3148`.
|
128
|
+
|
129
|
+
=== Reading from and writing to the feed
|
130
|
+
|
131
|
+
For the impatient, here is a quick way to get started with the `SimpleFeed`.
|
132
|
+
|
133
|
+
[source,ruby]
|
134
|
+
----
|
81
135
|
# This assumes we have previously defined a feed named :newsfeed (see above)
|
82
136
|
activity = SimpleFeed.newsfeed.activity(@current_user.id)
|
83
137
|
# Store directly the value and the optional time stamp
|
@@ -88,7 +142,7 @@ activity.store(value: 'hello')
|
|
88
142
|
@event = SimpleFeed::Event.new('hello', Time.now)
|
89
143
|
activity.store(event: @event)
|
90
144
|
# => false # false indicates that the same event is already in the feed.
|
91
|
-
|
145
|
+
----
|
92
146
|
|
93
147
|
As we've added events for this user, we can request them back, sorted by
|
94
148
|
the time and paginated. If you are using a distributed provider, such as
|
@@ -96,34 +150,37 @@ the time and paginated. If you are using a distributed provider, such as
|
|
96
150
|
application, not just the one that published the event (which is the
|
97
151
|
case for the "toy" `Hash::Provider`.
|
98
152
|
|
99
|
-
|
153
|
+
[source,ruby]
|
154
|
+
----
|
100
155
|
activity.paginate(page: 1, reset_last_read: true)
|
101
156
|
# => [ <SimpleFeed::Event#0x2134afa value='hello' at='2016-11-20 23:32:56 -0800'> ]
|
102
|
-
|
157
|
+
----
|
103
158
|
|
104
|
-
|
159
|
+
=== The Two Forms of the API
|
105
160
|
|
106
161
|
The feed API is offered in two forms:
|
107
162
|
|
108
|
-
|
109
|
-
|
163
|
+
. single-user form, and
|
164
|
+
. a batch (multi-user) form.
|
110
165
|
|
111
166
|
The method names and signatures are the same. The only difference is in what the methods return:
|
112
167
|
|
113
|
-
|
114
|
-
|
168
|
+
. In the single user case, the return of, say, `#total_count` is an `Integer` value representing the total count for this user.
|
169
|
+
. In the multi-user case, the return is a `SimpleFeed::Response` instance, that can be thought of as a `Hash`, that has the user IDs as the keys, and return results for each user as a value.
|
115
170
|
|
116
|
-
Please see further below the details about the
|
171
|
+
Please see further below the details about the <<batch-api,Batch API>>.
|
117
172
|
|
118
|
-
|
173
|
+
+++<a name="single-user-api">++++++</a>+++
|
119
174
|
|
120
|
-
|
175
|
+
[discrete]
|
176
|
+
===== Single-User API
|
121
177
|
|
122
178
|
In the examples below we show responses based on a single-user usage. As previously mentioned, the multi-user usage is the same, except what the response values are, and is discussed further down below.
|
123
179
|
|
124
180
|
Let's take a look at a ruby session, which demonstrates return values of the feed operations for a single user:
|
125
181
|
|
126
|
-
|
182
|
+
[source,ruby]
|
183
|
+
----
|
127
184
|
require 'simplefeed'
|
128
185
|
|
129
186
|
# Define the feed using an in-memory Hash provider, which uses
|
@@ -134,17 +191,21 @@ SimpleFeed.define(:followers) do |f|
|
|
134
191
|
f.per_page = 2
|
135
192
|
end
|
136
193
|
|
137
|
-
# Let's get the Activity instance that wraps this
|
138
|
-
activity = SimpleFeed.followers.activity(user_id)
|
194
|
+
# Let's get the Activity instance that wraps this
|
195
|
+
activity = SimpleFeed.followers.activity(user_id) # => [... complex object removed for brevity ]
|
196
|
+
|
139
197
|
# let's clear out this feed to ensure it's empty
|
140
198
|
activity.wipe # => true
|
199
|
+
|
141
200
|
# Let's verify that the counts for this feed are at zero
|
142
201
|
activity.total_count # => 0
|
143
202
|
activity.unread_count # => 0
|
203
|
+
|
144
204
|
# Store some events
|
145
205
|
activity.store(value: 'hello') # => true
|
146
206
|
activity.store(value: 'goodbye', at: Time.now - 20) # => true
|
147
207
|
activity.unread_count # => 2
|
208
|
+
|
148
209
|
# Now we can paginate the events, while resetting this user's last-read timestamp:
|
149
210
|
activity.paginate(page: 1, reset_last_read: true)
|
150
211
|
# [
|
@@ -159,49 +220,54 @@ activity.delete(value: 'hello') # => true
|
|
159
220
|
# array of all events that have been deleted for this user.
|
160
221
|
activity.delete_if do |event, user_id|
|
161
222
|
event.value =~ /good/
|
162
|
-
end
|
223
|
+
end
|
163
224
|
# => [
|
164
225
|
# [0] #<SimpleFeed::Event: value=good bye, at=1480475294.0579991>
|
165
|
-
# ]
|
226
|
+
# ]
|
166
227
|
activity.total_count # => 0
|
167
|
-
|
228
|
+
----
|
168
229
|
|
169
|
-
You can fetch all items (optionally filtered by time) in the feed using `#fetch`,
|
230
|
+
You can fetch all items (optionally filtered by time) in the feed using `#fetch`,
|
170
231
|
`#paginate` and reset the `last_read` timestamp by passing the `reset_last_read: true` as a parameter.
|
171
232
|
|
172
|
-
|
233
|
+
+++<a name="batch-api">++++++</a>+++
|
173
234
|
|
174
|
-
|
235
|
+
[discrete]
|
236
|
+
===== Batch (Multi-User) API
|
175
237
|
|
176
238
|
This API should be used when dealing with an array of users (or, in the
|
177
|
-
future, a Proc or an ActiveRecord relation).
|
239
|
+
future, a Proc or an ActiveRecord relation).
|
240
|
+
|
241
|
+
____
|
242
|
+
There are several reasons why this API should be preferred for
|
243
|
+
operations that perform a similar action across a range of users:
|
244
|
+
_various provider implementations can be heavily optimized for
|
245
|
+
concurrency, and performance_.
|
178
246
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
> concurrency, and performance_.
|
183
|
-
>
|
184
|
-
> The Redis Provider, for example, uses a notion of `pipelining` to send
|
185
|
-
> updates for different users asynchronously and concurrently.
|
247
|
+
The Redis Provider, for example, uses a notion of `pipelining` to send
|
248
|
+
updates for different users asynchronously and concurrently.
|
249
|
+
____
|
186
250
|
|
187
251
|
Multi-user operations return a `SimpleFeed::Response` object, which can
|
188
252
|
be used as a hash (keyed on user_id) to fetch the result of a given
|
189
253
|
user.
|
190
254
|
|
191
|
-
|
255
|
+
[source,ruby]
|
256
|
+
----
|
192
257
|
# Using the Feed API with, eg #find_in_batches
|
193
258
|
@event_producer.followers.find_in_batches do |group|
|
194
|
-
|
259
|
+
|
195
260
|
# Convert a group to the array of IDs and get ready to store
|
196
261
|
activity = SimpleFeed.get(:followers).activity(group.map(&:id))
|
197
262
|
activity.store(value: "#{@event_producer.name} liked an article")
|
198
|
-
|
199
|
-
# => [Response] { user_id1 => [Boolean], user_id2 => [Boolean]... }
|
263
|
+
|
264
|
+
# => [Response] { user_id1 => [Boolean], user_id2 => [Boolean]... }
|
200
265
|
# true if the value was stored, false if it wasn't.
|
201
266
|
end
|
202
|
-
|
267
|
+
----
|
203
268
|
|
204
|
-
|
269
|
+
[discrete]
|
270
|
+
===== Activity Feed DSL (Domain-Specific Language)
|
205
271
|
|
206
272
|
The library offers a convenient DSL for adding feed functionality into
|
207
273
|
your current scope.
|
@@ -211,7 +277,8 @@ exports just one primary method `#with_activity`. You call this method
|
|
211
277
|
and pass an activity object created for a set of users (or a single
|
212
278
|
user), like so:
|
213
279
|
|
214
|
-
|
280
|
+
[source,ruby]
|
281
|
+
----
|
215
282
|
require 'simplefeed/dsl'
|
216
283
|
include SimpleFeed::DSL
|
217
284
|
|
@@ -226,43 +293,46 @@ end
|
|
226
293
|
with_activity(activity, countries: data_to_store) do
|
227
294
|
# we can use countries as a variable because it was passed above in **opts
|
228
295
|
countries.each do |country|
|
229
|
-
# we can call #store without a receiver because the block is passed to
|
296
|
+
# we can call #store without a receiver because the block is passed to
|
230
297
|
# instance_eval
|
231
298
|
store(value: country) { |result| report(result ? 'success' : 'failure') }
|
232
|
-
# we can call #report inside the proc because it is evaluated in the
|
299
|
+
# we can call #report inside the proc because it is evaluated in the
|
233
300
|
# outside context of the #with_activity
|
234
|
-
|
235
|
-
# now let's print a color ASCII dump of the entire feed for this user:
|
236
|
-
color_dump
|
237
|
-
end
|
301
|
+
|
302
|
+
# now let's print a color ASCII dump of the entire feed for this user:
|
303
|
+
color_dump
|
304
|
+
end
|
238
305
|
printf "Activity counts are: %d unread of %d total\n", unread_count, total_count
|
239
306
|
end
|
240
|
-
|
307
|
+
----
|
241
308
|
|
242
|
-
The DSL context has access to two additional methods:
|
309
|
+
The DSL context has access to two additional methods:
|
243
310
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
311
|
+
* `#event(value, at)` returns a fully constructed `SimpleFeed::Event` instance
|
312
|
+
* `#color_dump` prints to STDOUT the ASCII text dump of the current user's activities (events), as well as the counts and the `last_read` shown visually on the time line.
|
313
|
+
|
314
|
+
[discrete]
|
315
|
+
===== `#color_dump`
|
248
316
|
|
249
317
|
Below is an example output of `color_dump` method, which is intended for the debugging purposes.
|
250
318
|
|
251
|
-
[
|
319
|
+
[image:https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-color-dump.png[color_dump output,450]](https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-color-dump.png)
|
252
320
|
|
253
|
-
|
321
|
+
+++<a name="api">++++++</a>+++
|
254
322
|
|
255
|
-
|
323
|
+
== Complete API
|
256
324
|
|
257
|
-
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.
|
325
|
+
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.
|
258
326
|
|
259
|
-
|
327
|
+
[discrete]
|
328
|
+
==== Multi-User (Batch) API
|
260
329
|
|
261
330
|
Each API call at this level expects an array of user IDs, therefore the
|
262
331
|
return value is an object, `SimpleFeed::Response`, containing individual
|
263
332
|
responses for each user, accessible via `response[user_id]` method.
|
264
333
|
|
265
|
-
|
334
|
+
[source,ruby]
|
335
|
+
----
|
266
336
|
@multi = SimpleFeed.get(:feed_name).activity(User.active.map(&:id))
|
267
337
|
|
268
338
|
@multi.store(value:, at:)
|
@@ -274,7 +344,7 @@ responses for each user, accessible via `response[user_id]` method.
|
|
274
344
|
# => [Response] { user_id => [Boolean], ... } true if the value was removed, false if it didn't exist
|
275
345
|
|
276
346
|
@multi.delete_if do |event, user_id|
|
277
|
-
# if the block returns true, the event is deleted and returned
|
347
|
+
# if the block returns true, the event is deleted and returned
|
278
348
|
end
|
279
349
|
# => [Response] { user_id => [deleted_event1, deleted_event2, ...], ... }
|
280
350
|
|
@@ -283,15 +353,15 @@ end
|
|
283
353
|
# => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
|
284
354
|
|
285
355
|
# Return a paginated list of all items, optionally with the total count of items
|
286
|
-
@multi.paginate(page: 1,
|
287
|
-
per_page: @multi.feed.per_page,
|
288
|
-
with_total: false,
|
356
|
+
@multi.paginate(page: 1,
|
357
|
+
per_page: @multi.feed.per_page,
|
358
|
+
with_total: false,
|
289
359
|
reset_last_read: false)
|
290
360
|
# => [Response] { user_id => [Array]<Event>, ... }
|
291
361
|
# Options:
|
292
362
|
# reset_last_read: false — reset last read to Time.now (true), or the provided timestamp
|
293
363
|
# with_total: true — returns a hash for each user_id:
|
294
|
-
# => [Response] { user_id => { events: Array<Event>, total_count: 3 }, ... }
|
364
|
+
# => [Response] { user_id => { events: Array<Event>, total_count: 3 }, ... }
|
295
365
|
|
296
366
|
# Return un-paginated list of all items, optionally filtered
|
297
367
|
@multi.fetch(since: nil, reset_last_read: false)
|
@@ -305,111 +375,106 @@ end
|
|
305
375
|
# => [Response] { user_id => [Time] last_read, ... }
|
306
376
|
|
307
377
|
@multi.total_count
|
308
|
-
# => [Response] { user_id => [Integer] total_count, ... }
|
378
|
+
# => [Response] { user_id => [Integer, String] total_count, ... }
|
309
379
|
|
310
380
|
@multi.unread_count
|
311
|
-
# => [Response] { user_id => [Integer] unread_count, ... }
|
381
|
+
# => [Response] { user_id => [Integer, String] unread_count, ... }
|
312
382
|
|
313
383
|
@multi.last_read
|
314
384
|
# => [Response] { user_id => [Time] last_read, ... }
|
385
|
+
----
|
315
386
|
|
316
|
-
|
317
|
-
|
318
|
-
## Providers in Depth
|
387
|
+
== Providers
|
319
388
|
|
320
|
-
As we've discussed above, a provider is an underlying persistence mechanism implementation.
|
389
|
+
As we've discussed above, a provider is an underlying persistence mechanism implementation.
|
321
390
|
|
322
391
|
It is the intention of this gem that:
|
323
392
|
|
324
|
-
|
325
|
-
|
393
|
+
* it should be easy to write new providers
|
394
|
+
* it should be easy to swap out providers
|
326
395
|
|
327
396
|
To create a new provider please use `SimpleFeed::Providers::Hash::Provider` class as a starting point.
|
328
397
|
|
329
398
|
Two providers are available with this gem:
|
330
399
|
|
331
|
-
|
400
|
+
=== `SimpleFeed::Providers::Redis::Provider`
|
332
401
|
|
333
|
-
Redis Provider is a production-ready persistence adapter that uses the
|
402
|
+
Redis Provider is a production-ready persistence adapter that uses the https://redislabs.com/ebook/redis-in-action/part-2-core-concepts-2/chapter-3-commands-in-redis/3-5-sorted-sets[sorted set Redis data type].
|
334
403
|
|
335
|
-
This provider is optimized for large writes and can use either a single Redis instance for all users of your application, or any number of Redis
|
404
|
+
This provider is optimized for large writes and can use either a single Redis instance for all users of your application, or any number of Redis https://en.wikipedia.org/wiki/Shard_(database_architecture)[shards] by using a https://github.com/twitter/twemproxy[_Twemproxy_] in front of the Redis shards.
|
336
405
|
|
337
|
-
|
406
|
+
=== `SimpleFeed::Providers::HashProvider`
|
338
407
|
|
339
|
-
|
340
|
-
|
408
|
+
This 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.
|
341
409
|
|
342
|
-
|
410
|
+
== Redis Provider
|
343
411
|
|
344
412
|
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.
|
345
|
-
|
346
|
-
|
413
|
+
|
414
|
+
== Running the Examples
|
347
415
|
|
348
416
|
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.
|
349
417
|
|
350
418
|
To run it, checkout the source of the library, and then:
|
351
419
|
|
352
|
-
|
420
|
+
[source,bash]
|
421
|
+
----
|
353
422
|
git clone https://github.com/kigster/simple-feed.git
|
354
423
|
cd simple-feed
|
355
424
|
bundle
|
356
425
|
be rspec # make sure tests are passing
|
357
|
-
ruby examples/redis_provider_example.rb
|
358
|
-
|
426
|
+
ruby examples/redis_provider_example.rb
|
427
|
+
----
|
359
428
|
|
360
|
-
The above command will help you download, setup all dependencies, and run the examples for a single user:
|
429
|
+
The above command will help you download, setup all dependencies, and run the examples for a single user:
|
361
430
|
|
362
|
-
|
431
|
+
image::https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example.png[Example,link=https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example.png]
|
363
432
|
|
364
433
|
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:
|
365
434
|
|
366
|
-
|
435
|
+
image::https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example-redis-debug.png[Example with Debugging,link=https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-example-redis-debug.png]
|
367
436
|
|
368
|
-
|
437
|
+
=== Generating Ruby API Documentation
|
369
438
|
|
370
|
-
|
439
|
+
[source,bash]
|
440
|
+
----
|
371
441
|
rake doc
|
372
|
-
|
442
|
+
----
|
373
443
|
|
374
444
|
This should use Yard to generate the documentation, and open your browser once it's finished.
|
375
445
|
|
376
|
-
|
446
|
+
=== Installation
|
377
447
|
|
378
448
|
Add this line to your application's Gemfile:
|
379
449
|
|
380
|
-
|
450
|
+
[source,ruby]
|
451
|
+
----
|
381
452
|
gem 'simple-feed'
|
382
|
-
|
453
|
+
----
|
383
454
|
|
384
455
|
And then execute:
|
385
456
|
|
386
|
-
|
387
|
-
$ bundle
|
388
|
-
```
|
457
|
+
$ bundle
|
389
458
|
|
390
459
|
Or install it yourself as:
|
391
460
|
|
392
|
-
|
393
|
-
$ gem install simple-feed
|
394
|
-
```
|
461
|
+
$ gem install simple-feed
|
395
462
|
|
396
|
-
|
463
|
+
=== Development
|
397
464
|
|
398
465
|
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.
|
399
466
|
|
400
|
-
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
|
467
|
+
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 https://rubygems.org[rubygems.org].
|
401
468
|
|
402
|
-
|
469
|
+
=== Contributing
|
403
470
|
|
404
471
|
Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/simple-feed
|
405
472
|
|
406
|
-
|
407
|
-
|
408
|
-
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
409
|
-
|
410
|
-
### Acknowledgements
|
473
|
+
=== License
|
411
474
|
|
412
|
-
|
413
|
-
* Author's personal experience at [Wanelo, Inc.](https://wanelo.com) has served as an inspiration.
|
475
|
+
The gem is available as open source under the terms of the http://opensource.org/licenses/MIT[MIT License].
|
414
476
|
|
477
|
+
=== Acknowledgements
|
415
478
|
|
479
|
+
* This project is conceived and sponsored by https://simbi.com[Simbi, Inc.].
|
480
|
+
* Author's personal experience at https://wanelo.com[Wanelo, Inc.] has served as an inspiration.
|