simple-feed 1.0.4 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 37f4c35a87e3c3ed5b0a3cbdc88b6a88edc795c3
4
- data.tar.gz: c88031baedfb60e6376d561621d2547b2db37978
3
+ metadata.gz: de6f18bbf9e002e7f1ee3fa49a7675ea66320bc4
4
+ data.tar.gz: 1325dfc4fed1fe485aef9d2fcd0a75442022caee
5
5
  SHA512:
6
- metadata.gz: eb887ba71541e037975ae5800842b2f5a824901c5134ad6e38cd91489917fb62971edefac865fa5d2f7e21452417d08c77d164caa84d84eb16c0f681bb12a239
7
- data.tar.gz: db3fad51130186c6199e6874b54e4659eedada7cf4e995781a5103e720e6beb712e0a610d30f5a2eed9abc9f8b23336f33007539e3825dc2ee3dad5901acce79
6
+ metadata.gz: d90c16eaca736fe56ae19a4c2df0688a9267f00863f18a713ac678ec9150210e94553945636450651563af35706f4ec68dcb113fa451a7109c7376bcb5704363
7
+ data.tar.gz: 758243acc5437bc4f022dfbeaa5b93d3af01525ec972760fe33cca1756de220e4b2b344c5e2f7e771578f8fdb24aef662e4abad8a9ba09ace74776c0512e37d8
data/.gitignore CHANGED
@@ -14,3 +14,4 @@
14
14
  ._*
15
15
  .Spotlight-V100
16
16
  .Trashes
17
+ /lib/simplefeed/providers/serialization/key.rb
data/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![Code Climate](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/gpa.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
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
+ [![Inline docs](http://inch-ci.org/github/kigster/simple-feed.svg?branch=master)](http://inch-ci.org/github/kigster/simple-feed)
9
10
 
10
11
  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
12
 
@@ -35,55 +36,50 @@ What you publish into your feed — i.e. _stories_ or _events_, will depend enti
35
36
 
36
37
  ## Challenges
37
38
 
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:
39
+ Building a personalized activity feed tends 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:
39
40
 
40
41
  * optimizing the read time performance by pre-computing the feed for each user ahead of time
41
42
  * OR optimizing the various ranking algorithms by computing the feed at read time, with complex forms of caching addressing the performance requirements.
42
43
 
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.
44
45
 
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
+ For more information about various types of feed, and the typical architectures that power them — please read:
47
+
48
+ - ["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).
49
+ - ["Feeding Frenzy: Selectively Materializing Users’ Event Feeds"](http://jeffterrace.com/docs/feeding-frenzy-sigmod10-web.pdf) (Yahoo! Research paper).
46
50
 
47
51
  ## Overview
48
52
 
49
53
  The feed library aims to address the following goals:
50
54
 
51
- * To define a minimalistic API for a typical event-based simple feed,
52
- without tying it to any concrete provider implementation
53
- * To make it easy to implement and plug in a new type of provider,
54
- eg.using Couchbase or MongoDB
55
+ * To define a minimalistic API for a typical event-based simple feed, without tying it to any concrete provider implementation
56
+ * To make it easy to implement and plug in a new type of provider, eg. using *Couchbase* or *MongoDB*
55
57
  * To provide a scalable default provider implementation using Redis, which can support millions of users via sharding
56
58
  * 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.
57
59
 
58
60
  ## Usage
59
61
 
60
- First you need to configure the Feed with a valid provider
61
- implementation and a name.
62
+ First you need to configure the Feed with a valid provider implementation and a name.
62
63
 
63
64
  ### Configuration
64
65
 
65
- Below we configure a feed called `:newsfeed`, which presumably
66
- will be populated with the events coming from the followers.
66
+ Below we configure a feed called `:newsfeed`, which presumably will be populated with the events coming from the followers.
67
67
 
68
68
  ```ruby
69
69
  require 'simplefeed'
70
- require 'simplefeed/providers/redis'
71
70
 
72
71
  # Let's define a Redis-based feed, and wrap Redis in a in a ConnectionPool.
73
72
  SimpleFeed.define(:newsfeed) do |f|
74
73
  f.provider = SimpleFeed.provider(:redis,
75
74
  redis: -> { ::Redis.new },
76
75
  pool_size: 10)
77
- f.per_page = 50
78
- f.batch_size = 10 # default batch size
79
- f.namespace = 'nf'
76
+ f.per_page = 50 # default page size
77
+ f.batch_size = 10 # default batch size
78
+ f.namespace = 'nf' # only needed if you use the same redis for more than one feed
80
79
  end
81
80
  ```
82
81
 
83
- After the feed is defined, the gem creates a similarly named method
84
- under the `SimpleFeed` namespace to access the feed. For example, given
85
- a name such as `:newsfeed` the following are all valid ways of
86
- accessing the feed:
82
+ 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:
87
83
 
88
84
  * `SimpleFeed.newsfeed`
89
85
  * `SimpleFeed.get(:newsfeed)`
@@ -92,21 +88,17 @@ You can also get a full list of currently defined feeds with `SimpleFeed.feed_na
92
88
 
93
89
  ### Reading from and writing to the feed
94
90
 
95
- For the impatient here is a quick way to get started with the
96
- `SimpleFeed`.
91
+ For the impatient here is a quick way to get started with the `SimpleFeed`.
97
92
 
98
93
  ```ruby
99
- activity = SimpleFeed.get(:followers).activity(@current_user.id)
100
-
94
+ # This assumes we have previously defined a feed named :newsfeed (see above)
95
+ activity = SimpleFeed.newsfeed.activity(@current_user.id)
101
96
  # Store directly the value and the optional time stamp
102
97
  activity.store(value: 'hello')
103
98
  # => true
104
99
 
105
100
  # or equivalent:
106
- @event = SimpleFeed::Event.new(value: 'hello', at: Time.now)
107
- # or even simpler:
108
101
  @event = SimpleFeed::Event.new('hello', Time.now)
109
- # and then:
110
102
  activity.store(event: @event)
111
103
  # => false # false indicates that the same event is already in the feed.
112
104
  ```
@@ -124,25 +116,25 @@ activity.paginate(page: 1)
124
116
 
125
117
  ### The Two Forms of the API
126
118
 
127
- The feed API is offered in a single-user and a batch (multi-user) forms.
119
+ The feed API is offered in two forms:
120
+
121
+ 1. single-user form, and
122
+ 2. a batch (multi-user) form.
128
123
 
129
- The main and only difference is in what the methods return. In the
130
- single user case, the return of, say, `#total_count` is an `Integer`
131
- value representing the total count for this user.
124
+ The method names and signatures are the same. The only difference is in what the methods return:
132
125
 
133
- In the multi-user case, the return is a `SimpleFeed::Response` instance,
134
- that can be thought of as a `Hash`, that has the user IDs as the keys,
135
- and return results for each user as a value.
126
+ 1. In the single user case, the return of, say, `#total_count` is an `Integer` value representing the total count for this user.
127
+ 2. 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.
136
128
 
137
- Please see further below the details about the [Batch API](#bach-api).
129
+ Please see further below the details about the [Batch API](#batch-api).
138
130
 
139
131
  <a name="single-user-api"/>
140
132
 
141
133
  ##### Single-User API
142
134
 
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.
135
+ 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.
144
136
 
145
- Below is a user session that demonstrates simple return values from the feed operations for a given user:
137
+ Let's take a look at a ruby session, which demonstrates return values of the feed operations for a single user:
146
138
 
147
139
  ```ruby
148
140
  require 'simplefeed'
@@ -156,7 +148,7 @@ SimpleFeed.define(:followers) do |f|
156
148
  end
157
149
 
158
150
  # Let's get the Activity instance that wraps this user_id
159
- activity = SimpleFeed.get(:followers).activity(user_id) # => [... complex object removed for brevity ]
151
+ activity = SimpleFeed.followers.activity(user_id) # => [... complex object removed for brevity ]
160
152
  # let's clear out this feed to ensure it's empty
161
153
  activity.wipe # => true
162
154
  # Let's verify that the counts for this feed are at zero
@@ -164,23 +156,31 @@ activity.total_count # => 0
164
156
  activity.unread_count # => 0
165
157
  # Store some events
166
158
  activity.store(value: 'hello') # => true
167
- activity.store(value: 'goodbye') # => true
159
+ activity.store(value: 'goodbye', at: Time.now - 20) # => true
168
160
  activity.unread_count # => 2
169
161
  # Now we can paginate the events, which by default resets "last_read" timestamp the user
170
162
  activity.paginate(page: 1)
171
163
  # [
172
- # [0] #<SimpleFeed::Event#70138821650220 {"value":"goodbye","at":1480475294.0579991}>,
173
- # [1] #<SimpleFeed::Event#70138821649420 {"value":"hello","at":1480475294.057138}>
164
+ # [0] #<SimpleFeed::Event: value=good bye, at=1480475294.0579991>,
165
+ # [1] #<SimpleFeed::Event: value=hello, at=1480475294.057138>
174
166
  # ]
175
167
  # Now the unread_count should return 0 since the user just "viewed" the feed.
176
168
  activity.unread_count # => 0
177
169
  activity.delete(value: 'hello') # => true
178
- activity.total_count # => 1
170
+ # the next method yields to a passed in block for each event in the user's feed, and deletes
171
+ # all events for which the block returns true. The return of this call is the
172
+ # array of all events that have been deleted for this user.
173
+ activity.delete_if do |event, user_id|
174
+ event.value =~ /good/
175
+ end
176
+ # => [
177
+ # [0] #<SimpleFeed::Event: value=good bye, at=1480475294.0579991>
178
+ # ]
179
+ activity.total_count # => 0
179
180
  ```
180
181
 
181
- You can fetch all items in the feed using `#fetch`, and you can
182
- `#paginate` without resetting the `last_read` timestamp by passing the
183
- `peek: true` as a parameter.
182
+ You can fetch all items (optionally filtered by time) in the feed using `#fetch`, and you can
183
+ `#paginate` and reset the `last_read` timestamp by passing the `reset_last_read: true` as a parameter.
184
184
 
185
185
  <a name="batch-api"/>
186
186
 
@@ -214,13 +214,13 @@ user.
214
214
  end
215
215
  ```
216
216
 
217
- ##### DSL
217
+ ##### Activity Feed DSL (Domain-Specific Language)
218
218
 
219
219
  The library offers a convenient DSL for adding feed functionality into
220
220
  your current scope.
221
221
 
222
222
  To use the module, just include `SimpleFeed::DSL` where needed, which
223
- exports just one primary method `with_activity'. You call this method
223
+ exports just one primary method `#with_activity`. You call this method
224
224
  and pass an activity object created for a set of users (or a single
225
225
  user), like so:
226
226
 
@@ -252,6 +252,17 @@ with_activity(activity, countries: data_to_store) do
252
252
  end
253
253
  ```
254
254
 
255
+ The DSL context has access to two additional methods:
256
+
257
+ * `#event(value, at)` returns a fully constructed `SimpleFeed::Event` instance
258
+ * `#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.
259
+
260
+ ##### `#color_dump`
261
+
262
+ Below is an example output of `color_dump` method, which is intended for the debugging purposes.
263
+
264
+ [<img src="https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-color-dump.png" width="450" alt="color_dump output" style="width: 300px; max-width:100%;">](https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-color-dump.png)
265
+
255
266
  <a name="api"/>
256
267
 
257
268
  ## Complete API
@@ -275,28 +286,30 @@ responses for each user, accessible via `response[user_id]` method.
275
286
  @multi.delete(event:)
276
287
  # => [Response] { user_id => [Boolean], ... } true if the value was removed, false if it didn't exist
277
288
 
278
- @multi.delete_if do |user_id, event|
279
- # if the block returns true, the event is deleted
289
+ @multi.delete_if do |event, user_id|
290
+ # if the block returns true, the event is deleted and returned
280
291
  end
292
+ # => [Response] { user_id => [deleted_event1, deleted_event2, ...], ... }
281
293
 
282
294
  # Wipe the feed for a given user(s)
283
295
  @multi.wipe
284
296
  # => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
285
297
 
286
298
  # 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)
299
+ @multi.paginate(page:, per_page:, with_total: false, reset_last_read: false)
288
300
  # => [Response] { user_id => [Array]<Event>, ... }
289
301
  # Options:
290
- # peek: truedoes not reset last_read, otherwise it does.
302
+ # reset_last_read: falsereset last read to Time.now (true), or the provided timestamp
291
303
  # with_total: true — returns a hash for each user_id:
292
304
  # => [Response] { user_id => { events: Array<Event>, total_count: 3 }, ... }
293
305
 
294
306
  # Return un-paginated list of all items, optionally filtered
295
- @multi.fetch(since: nil)
307
+ @multi.fetch(since: nil, reset_last_read: false)
296
308
  # => [Response] { user_id => [Array]<Event>, ... }
297
309
  # Options:
310
+ # reset_last_read: false — reset last read to Time.now (true), or the provided timestamp
298
311
  # since: <timestamp> — if provided, returns all items posted since then
299
- # since: :unread — if provided, returns all unread items and resets +last_read+
312
+ # since: :last_read — if provided, returns all unread items and resets +last_read+
300
313
 
301
314
  @multi.reset_last_read
302
315
  # => [Response] { user_id => [Time] last_read, ... }
@@ -7,35 +7,51 @@ require 'simplefeed/providers/redis'
7
7
  require 'simplefeed/providers/hash'
8
8
  require 'simplefeed/dsl'
9
9
 
10
+ # Main namespace module for the SimpleFeed gem. It provides several shortcuts and entry
11
+ # points into the library, such as ability to define and fetch new feeds via +define+,
12
+ # and so on.
10
13
  module SimpleFeed
11
14
  @registry = {}
12
15
 
13
- def self.registry
14
- @registry
15
- end
16
+ class << self
17
+ # @return [Hash<Symbol, Feed>] the registry of the defined feeds
18
+ def registry
19
+ @registry
20
+ end
16
21
 
17
- def self.define(name, **options, &block)
18
- name = name.to_sym unless name.is_a?(Symbol)
19
- feed = registry[name] ? registry[name] : SimpleFeed::Feed.new(name)
20
- feed.configure(options) do
21
- block.call(feed) if block
22
+ # @param name [Symbol] feed name
23
+ # @param options [Hash] any key-value pairs to set on the feed
24
+ #
25
+ # @return [Feed] the feed with the given name, and defined via options and a block
26
+ def define(name, **options, &block)
27
+ name = name.to_sym unless name.is_a?(Symbol)
28
+ feed = registry[name] ? registry[name] : SimpleFeed::Feed.new(name)
29
+ feed.configure(options) do
30
+ block.call(feed) if block
31
+ end
32
+ registry[name] = feed
33
+ feed
22
34
  end
23
- registry[name] = feed
24
- feed
25
- end
26
35
 
27
- def self.get(name)
28
- registry[name.to_sym]
29
- end
36
+ # @return [Feed] the pre-defined feed with the given name
37
+ def get(name)
38
+ registry[name.to_sym]
39
+ end
30
40
 
31
- def self.provider(provider_name, *args, **opts, &block)
32
- provider_class = SimpleFeed::Providers.registry[provider_name]
33
- raise ArgumentError, "No provider named #{provider_name} was found, #{SimpleFeed::Providers.registry.inspect}" unless provider_class
34
- provider_class.new(*args, **opts, &block)
35
- end
41
+ # A factory method that constructs an instance of a provider based on the provider name and arguments.
42
+ #
43
+ # @param provider_name [Symbol] short name of the provider, eg, :redis, :hash, etc.
44
+ # @params args [Array] constructor array arguments of the provider
45
+ # @params opts [Hash] constructor hash arguments of the provider
46
+ #
47
+ # @return [Provider]
48
+ def provider(provider_name, *args, **opts, &block)
49
+ provider_class = SimpleFeed::Providers.registry[provider_name]
50
+ raise ArgumentError, "No provider named #{provider_name} was found, #{SimpleFeed::Providers.registry.inspect}" unless provider_class
51
+ provider_class.new(*args, **opts, &block)
52
+ end
36
53
 
37
- class << self
38
- # Forward all other method calls to Provider
54
+ # Forward all other method calls to the Provider
39
55
  def method_missing(name, *args, &block)
40
56
  registry[name] || super
41
57
  end
@@ -47,6 +63,7 @@ module SimpleFeed
47
63
  klass.instance_methods.grep(%r{[^=!]=$}).map { |m| m.to_s.gsub(/=/, '').to_sym }
48
64
  end
49
65
 
66
+ # Shortcut method to symbolize hash keys, using Hashie::Extensions
50
67
  def self.symbolize!(hash)
51
68
  Hashie::Extensions::SymbolizeKeys.symbolize_keys!(hash)
52
69
  end
@@ -27,24 +27,26 @@ module SimpleFeed
27
27
  # @multi.delete(event:)
28
28
  # # => [Response] { user_id => [Boolean], ... } true if the value was removed, false if it didn't exist
29
29
  #
30
- # @multi.delete_if do |user_id, event|
31
- # # if the block returns true, the event is deleted
30
+ # @multi.delete_if do |event, user_id|
31
+ # # if the block returns true, the event is deleted and returned
32
32
  # end
33
+ # # => [Response] { user_id => [Array]<Event>, ... }
33
34
  #
34
35
  # @multi.wipe
35
36
  # # => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
36
37
  #
37
- # @multi.paginate(page:, per_page:, peek: false, with_total: false)
38
+ # @multi.paginate(page:, per_page:, reset_last_read: [Bool | Time], with_total: false)
38
39
  # # => [Response] { user_id => [Array]<Event>, ... }
39
40
  # # Options:
40
- # # peek: truedoes not reset last_read, otherwise it does.
41
+ # # reset_last_read: falsereset last read to Time.now (true), or provided timestamp
41
42
  # # with_total: true — returns a hash for each user_id:
42
43
  # # => [Response] { user_id => { events: Array<Event>, total_count: 3 }, ... }
43
44
  #
44
45
  # # Return un-paginated list of all items, optionally filtered
45
- # @multi.fetch(since: nil)
46
+ # @multi.fetch(since: nil, reset_last_read: [Bool | Time] )
46
47
  # # => [Response] { user_id => [Array]<Event>, ... }
47
48
  # # Options:
49
+ # # reset_last_read: false — reset last read to Time.now (true), or provided timestamp
48
50
  # # since: <timestamp> — if provided, returns all items posted since then
49
51
  # # since: :unread — if provided, returns all unread items
50
52
  #
@@ -34,17 +34,18 @@ module SimpleFeed
34
34
  # @activity.wipe
35
35
  # # => [Boolean] true if user activity was found and deleted, false otherwise
36
36
  #
37
- # @activity.paginate(page:, per_page:, peek: false, with_total: false)
37
+ # @activity.paginate(page:, per_page:, reset_last_read: false, with_total: false)
38
38
  # # => [Array]<Event>
39
39
  # # Options:
40
- # # peek: truedoes not reset last_read, otherwise it does.
40
+ # # reset_last_read: falsereset last read to Time.now (true), or the provided timestamp
41
41
  # # with_total: true — returns a hash for each user_id:
42
42
  # # => { events: Array<Event>, total_count: 3 }
43
43
  #
44
44
  # # Return un-paginated list of all items, optionally filtered
45
- # @activity.fetch(since: nil)
45
+ # @activity.fetch(since: nil, reset_last_read: false)
46
46
  # # => [Array]<Event>
47
47
  # # Options:
48
+ # # reset_last_read: false — reset last read to Time.now (true), or the provided timestamp
48
49
  # # since: <timestamp> — if provided, returns all items posted since then
49
50
  # # since: :unread — if provided, returns all unread items
50
51
  #
@@ -1,5 +1,7 @@
1
1
  require_relative 'providers'
2
2
  require_relative 'activity/base'
3
+ require 'simplefeed/key/template'
4
+
3
5
  module SimpleFeed
4
6
  class Feed
5
7
 
@@ -11,15 +13,19 @@ module SimpleFeed
11
13
  end
12
14
 
13
15
  def initialize(name)
14
- @name = name
15
- @name = name.underscore.to_sym unless name.is_a?(Symbol)
16
+ @name = name
17
+ @name = name.underscore.to_sym unless name.is_a?(Symbol)
16
18
  # set the defaults if not passed in
17
- @meta = {}
18
- @namespace = nil
19
- @per_page ||= 50
20
- @max_size ||= 1000
21
- @batch_size||= 10
22
- @proxy = nil
19
+ @meta = {}
20
+ @namespace = nil
21
+ @per_page ||= 50
22
+ @max_size ||= 1000
23
+ @batch_size ||= 10
24
+ @proxy = nil
25
+ end
26
+
27
+ def key_template
28
+ SimpleFeed::Key::Template.new(namespace)
23
29
  end
24
30
 
25
31
  def provider=(definition)
@@ -32,6 +38,10 @@ module SimpleFeed
32
38
  @proxy
33
39
  end
34
40
 
41
+ def key(user_id)
42
+ SimpleFeed::Providers::Key.new(user_id, key_template)
43
+ end
44
+
35
45
  def provider_type
36
46
  SimpleFeed::Providers::Base::Provider.class_to_registry(@proxy.provider.class)
37
47
  end
@@ -0,0 +1,52 @@
1
+ require 'base62-rb'
2
+ require 'hashie/mash'
3
+
4
+ module SimpleFeed
5
+ module Key
6
+
7
+ class TextTemplate < Struct.new(:text)
8
+ def render(params = {})
9
+ output = self.text.dup
10
+ params.each_pair do |key, value|
11
+ output.gsub!(%r[{{\s*#{key}\s*}}], value.to_s)
12
+ end
13
+ output
14
+ end
15
+ end
16
+
17
+ DEFAULT_TEXT_TEMPLATE = TextTemplate.new('{{ namespace }}u.{{ base62_user_id }}.{{ key_marker }}')
18
+
19
+ class Template
20
+ attr_accessor :namespace, :key_types, :text_template
21
+
22
+ def initialize(namespace,
23
+ key_types = DEFAULT_TYPES,
24
+ text_template = DEFAULT_TEXT_TEMPLATE
25
+ )
26
+
27
+ self.namespace = namespace
28
+ self.key_types = key_types
29
+ self.text_template = text_template
30
+
31
+ self.key_types.each do |type|
32
+ type.template ||= text_template if text_template
33
+ end
34
+ end
35
+
36
+ def render_options
37
+ h = {}
38
+ h.merge!({ 'namespace' => namespace ? "#{namespace}|" : '' })
39
+ h
40
+ end
41
+
42
+ # Returns array of key names, such as [:meta, :data]
43
+ def key_names
44
+ key_types.map(&:name).map(&:to_s).sort
45
+ end
46
+
47
+ private
48
+
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,26 @@
1
+ require_relative 'template'
2
+
3
+ module SimpleFeed
4
+ module Key
5
+ class Type < Struct.new(:name, :marker)
6
+ attr_accessor :template
7
+
8
+ def initialize(name, marker, template = nil)
9
+ super(name, marker)
10
+ self.template = template
11
+ end
12
+
13
+ def render(opts = {})
14
+ self.template.render(opts.merge({ 'key_type' => name, 'key_marker' => marker }))
15
+ end
16
+ end
17
+
18
+ DEFAULT_TYPES = [
19
+ { name: :data, marker: 'd' },
20
+ { name: :meta, marker: 'm' }
21
+ ].map do |type|
22
+ Type.new(type[:name], type[:marker], DEFAULT_TEXT_TEMPLATE)
23
+ end
24
+
25
+ end
26
+ end
@@ -1,4 +1,4 @@
1
- require_relative 'providers/serialization/key'
1
+ require_relative 'providers/key'
2
2
  require_relative 'providers/proxy'
3
3
 
4
4
  module SimpleFeed
@@ -13,10 +13,6 @@ module SimpleFeed
13
13
  self.registry[provider_name] = provider_class
14
14
  end
15
15
 
16
- def self.key(*args)
17
- SimpleFeed::Providers::Serialization::Key.new(*args)
18
- end
19
-
20
16
  # These methods must be implemented by each Provider, and operation on a given
21
17
  # set of users passed via the user_ids: parameter.
22
18
  ACTIVITY_METHODS = %i(store delete delete_if wipe reset_last_read last_read paginate fetch total_count unread_count)
@@ -1,4 +1,4 @@
1
- require 'simplefeed/providers/serialization/key'
1
+ require 'simplefeed/providers/key'
2
2
 
3
3
  module SimpleFeed
4
4
  module Providers
@@ -28,13 +28,25 @@ module SimpleFeed
28
28
 
29
29
  protected
30
30
 
31
+ def reset_last_read_value(user_ids:, at: nil)
32
+ at = [Time, DateTime, Date].include?(at.class) ? at : Time.now
33
+ at = at.to_time if at.respond_to?(:to_time)
34
+ at = at.to_f if at.respond_to?(:to_f)
35
+
36
+ if self.respond_to?(:reset_last_read)
37
+ reset_last_read(user_ids: user_ids, at: at)
38
+ else
39
+ raise ArgumentError, "Class #{self.class} does not implement #reset_last_read method"
40
+ end
41
+ end
42
+
31
43
  def tap(value)
32
44
  yield
33
45
  value
34
46
  end
35
47
 
36
48
  def key(user_id)
37
- ::SimpleFeed::Providers.key(user_id, feed.namespace)
49
+ feed.key(user_id)
38
50
  end
39
51
 
40
52
  def to_array(user_ids)
@@ -73,11 +85,6 @@ module SimpleFeed
73
85
  response
74
86
  end
75
87
 
76
- def with_result
77
- result = yield
78
- result = transform_response(nil, result) if self.respond_to?(:transform_response)
79
- result
80
- end
81
88
  end
82
89
  end
83
90
  end
@@ -9,7 +9,7 @@ end
9
9
 
10
10
  require 'simplefeed/event'
11
11
  require_relative 'paginator'
12
- require_relative '../serialization/key'
12
+ require_relative '../key'
13
13
  require_relative '../base/provider'
14
14
 
15
15
  module SimpleFeed
@@ -47,9 +47,14 @@ module SimpleFeed
47
47
 
48
48
  def delete_if(user_ids:, &block)
49
49
  with_response_batched(user_ids) do |key|
50
- activity(key).each do |event|
51
- __delete(key, event) if yield(key.user_id, event)
52
- end
50
+ activity(key).map do |event|
51
+ if yield(event, key.user_id)
52
+ __delete(key, event)
53
+ event
54
+ else
55
+ nil
56
+ end
57
+ end.compact
53
58
  end
54
59
  end
55
60
 
@@ -61,27 +66,34 @@ module SimpleFeed
61
66
  end
62
67
  end
63
68
 
64
- def fetch(user_ids:, since: nil)
69
+ def paginate(user_ids:,
70
+ page:,
71
+ per_page: feed.per_page,
72
+ with_total: false,
73
+ reset_last_read: false)
74
+
75
+ reset_last_read_value(user_ids: user_ids, at: reset_last_read) if reset_last_read
76
+
65
77
  with_response_batched(user_ids) do |key|
78
+ activity = activity(key)
79
+ result = (page && page > 0) ? activity[((page - 1) * per_page)...(page * per_page)] : activity
80
+ with_total ? { events: result, total_count: activity.length } : result
81
+ end
82
+ end
83
+
84
+ def fetch(user_ids:, since: nil, reset_last_read: false)
85
+ response = with_response_batched(user_ids) do |key|
66
86
  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
87
+ activity(key).reject { |event| event.at < user_record(key).last_read.to_f }
70
88
  elsif since
71
89
  activity(key).reject { |event| event.at < since.to_f }
72
90
  else
73
91
  activity(key)
74
92
  end
75
93
  end
76
- end
94
+ reset_last_read_value(user_ids: user_ids, at: reset_last_read) if reset_last_read
77
95
 
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
80
- with_response_batched(user_ids) do |key|
81
- activity = activity(key)
82
- result = (page && page > 0) ? activity[((page - 1) * per_page)...(page * per_page)] : activity
83
- with_total ? { events: result, total_count: activity.length } : result
84
- end
96
+ response
85
97
  end
86
98
 
87
99
  def reset_last_read(user_ids:, at: Time.now)
@@ -0,0 +1,72 @@
1
+ require 'base62-rb'
2
+ require 'hashie/mash'
3
+ require 'simplefeed/key/template'
4
+ require 'simplefeed/key/type'
5
+
6
+ require 'forwardable'
7
+
8
+ module SimpleFeed
9
+ module Providers
10
+ # Here is a meta key for a given user ID:
11
+ #
12
+ # user 'm' for meta
13
+ # ↓ ↓
14
+ # "ff|u.f23098.m"
15
+ # ↑ ↑
16
+ # namespace user_id(base62)
17
+ #
18
+ class Key
19
+ attr_accessor :user_id, :key_template
20
+
21
+ extend Forwardable
22
+ def_delegators :@key_template, :key_names, :key_types
23
+
24
+ def initialize(user_id, key_template)
25
+ self.user_id = user_id
26
+ self.key_template = key_template
27
+
28
+ define_key_methods
29
+ end
30
+
31
+ def define_key_methods
32
+ key_template.key_types.each do |type|
33
+ key_name = type.name
34
+ unless self.respond_to?(key_name)
35
+ self.class.send(:define_method, key_name) do
36
+ instance_variable_get("@#{key_name}") ||
37
+ instance_variable_set("@#{key_name}", type.render(render_options))
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ def base62_user_id
45
+ @base62_user_id ||= ::Base62.encode(user_id)
46
+ end
47
+
48
+ def keys
49
+ key_names.map { |name| self.send(name) }
50
+ end
51
+
52
+ def render_options
53
+ key_template.render_options.merge!({
54
+ 'user_id' => user_id,
55
+ 'base62_user_id' => base62_user_id
56
+ })
57
+ end
58
+
59
+ def to_s
60
+ super + { user_id: user_id, base62_user_id: base62_user_id, keys: keys }.to_s
61
+ end
62
+
63
+ def inspect
64
+ render_options.inspect
65
+ end
66
+
67
+ private
68
+
69
+ end
70
+ end
71
+ end
72
+
@@ -4,7 +4,7 @@ require 'forwardable'
4
4
  require 'redis/pipeline' # defines Redis::Future
5
5
 
6
6
  require 'simplefeed/providers/base/provider'
7
- require 'simplefeed/providers/serialization/key'
7
+ require 'simplefeed/providers/key'
8
8
 
9
9
  require_relative 'driver'
10
10
  require_relative 'stats'
@@ -51,23 +51,29 @@ module SimpleFeed
51
51
  def delete_if(user_ids:)
52
52
  raise ArgumentError, '#delete_if must be called with a block that receives (user_id, event) as arguments.' unless block_given?
53
53
  with_response_batched(user_ids) do |key|
54
- fetch(user_ids: [key.user_id])[key.user_id].each do |event|
54
+ fetch(user_ids: [key.user_id])[key.user_id].map do |event|
55
55
  with_redis do |redis|
56
- yield(key.user_id, event) ? redis.zrem(key.data, event.value) : false
56
+ if yield(event, key.user_id)
57
+ redis.zrem(key.data, event.value) ? event : nil
58
+ end
57
59
  end
58
- end
60
+ end.compact
59
61
  end
60
62
  end
61
63
 
62
64
  def wipe(user_ids:)
63
65
  with_response_pipelined(user_ids) do |redis, key|
64
- should_wipe = block_given? ? yield(key.user_id) : true
65
- key.keys.all? { |redis_key| redis.del(redis_key) if should_wipe }
66
+ key.keys.all? { |redis_key| redis.del(redis_key) }
66
67
  end
67
68
  end
68
69
 
69
- def paginate(user_ids:, page:, per_page: feed.per_page, peek: false, with_total: false)
70
- reset_last_read(user_ids: user_ids) unless peek
70
+ def paginate(user_ids:, page:,
71
+ per_page: feed.per_page,
72
+ with_total: false,
73
+ reset_last_read: false)
74
+
75
+ reset_last_read_value(user_ids: user_ids, at: reset_last_read) if reset_last_read
76
+
71
77
  with_response_pipelined(user_ids) do |redis, key|
72
78
  events = paginated_events(page, per_page, redis, key)
73
79
  with_total ? { events: events,
@@ -75,14 +81,14 @@ module SimpleFeed
75
81
  end
76
82
  end
77
83
 
78
- def fetch(user_ids:, since: nil)
84
+ def fetch(user_ids:, since: nil, reset_last_read: false)
79
85
  if since == :unread
80
86
  last_read_response = with_response_pipelined(user_ids) do |redis, key|
81
87
  get_users_last_read(redis, key)
82
88
  end
83
- reset_last_read(user_ids: user_ids)
84
89
  end
85
- with_response_pipelined(user_ids) do |redis, key|
90
+
91
+ response = with_response_pipelined(user_ids) do |redis, key|
86
92
  if since == :unread
87
93
  redis.zrevrangebyscore(key.data, '+inf', (last_read_response.delete(key.user_id) || 0).to_f, withscores: true)
88
94
  elsif since
@@ -91,6 +97,10 @@ module SimpleFeed
91
97
  redis.zrevrange(key.data, 0, -1, withscores: true)
92
98
  end
93
99
  end
100
+
101
+ reset_last_read_value(user_ids: user_ids, at: reset_last_read) if reset_last_read
102
+
103
+ response
94
104
  end
95
105
 
96
106
  def reset_last_read(user_ids:, at: Time.now)
@@ -25,7 +25,7 @@ module SimpleFeed
25
25
  end
26
26
 
27
27
  def for(key_or_user_id, result = nil)
28
- user_id = key_or_user_id.is_a?(SimpleFeed::Providers::Serialization::Key) ?
28
+ user_id = key_or_user_id.is_a?(SimpleFeed::Providers::Key) ?
29
29
  key_or_user_id.user_id :
30
30
  key_or_user_id
31
31
 
@@ -1,3 +1,3 @@
1
1
  module SimpleFeed
2
- VERSION = '1.0.4'
2
+ VERSION = '2.0.1'
3
3
  end
Binary file
@@ -23,8 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ['lib']
24
24
 
25
25
  spec.add_dependency 'base62-rb'
26
- spec.add_dependency 'hiredis', '~> 0.6.0'
27
- spec.add_dependency 'redis', '>= 3.2'
26
+ spec.add_dependency 'hiredis'
27
+ spec.add_dependency 'redis'
28
28
  spec.add_dependency 'hashie'
29
29
  spec.add_dependency 'connection_pool', '~> 2'
30
30
  spec.add_dependency 'activesupport'
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
4
+ version: 2.0.1
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-13 00:00:00.000000000 Z
11
+ date: 2016-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base62-rb
@@ -28,30 +28,30 @@ dependencies:
28
28
  name: hiredis
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.6.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.6.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: redis
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '3.2'
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '3.2'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: hashie
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -273,23 +273,26 @@ files:
273
273
  - lib/simplefeed/dsl/formatter.rb
274
274
  - lib/simplefeed/event.rb
275
275
  - lib/simplefeed/feed.rb
276
+ - lib/simplefeed/key/template.rb
277
+ - lib/simplefeed/key/type.rb
276
278
  - lib/simplefeed/providers.rb
277
279
  - lib/simplefeed/providers/base/provider.rb
278
280
  - lib/simplefeed/providers/hash.rb
279
281
  - lib/simplefeed/providers/hash/paginator.rb
280
282
  - lib/simplefeed/providers/hash/provider.rb
283
+ - lib/simplefeed/providers/key.rb
281
284
  - lib/simplefeed/providers/proxy.rb
282
285
  - lib/simplefeed/providers/redis.rb
283
286
  - lib/simplefeed/providers/redis/boot_info.yml
284
287
  - lib/simplefeed/providers/redis/driver.rb
285
288
  - lib/simplefeed/providers/redis/provider.rb
286
289
  - lib/simplefeed/providers/redis/stats.rb
287
- - lib/simplefeed/providers/serialization/key.rb
288
290
  - lib/simplefeed/response.rb
289
291
  - lib/simplefeed/version.rb
290
292
  - man/activity-feed-action.png
291
293
  - man/running-example-redis-debug.png
292
294
  - man/running-example.png
295
+ - man/sf-color-dump.png
293
296
  - simple-feed.gemspec
294
297
  homepage: https://github.com/kigster/simple-feed
295
298
  licenses:
@@ -1,82 +0,0 @@
1
- require 'base62-rb'
2
- require 'hashie/mash'
3
- module SimpleFeed
4
- module Providers
5
- module Serialization
6
- class Key
7
- attr_accessor :user_id, :short_id, :prefix, :config
8
-
9
- # This hash defines the paramters for the strategy we use
10
- # to create a compact key based on the user id, feed's namespace,
11
- # and the functionality, such as 'm' meta — as a hash for storing
12
- # arbitrary values (in particular, +last_read+). and 'd' data —
13
- # as a sorted set in for storing the actual events.
14
- #
15
- # ### Examples
16
- #
17
- # Here is a meta key for a given user ID:
18
- #
19
- # user 'm' for meta
20
- # ↓ ↓
21
- # "ff|u.f23098.m"
22
- # ↑ ↑
23
- # namespace user_id(base62)
24
- #
25
- KEY_CONFIG = Hashie::Mash.new({
26
- separator: '.',
27
- prefix: '',
28
- namespace_divider: '|',
29
- namespace: nil,
30
- primary: ->(user_id) { ::Base62.encode(user_id) },
31
- primary_marker: 'u',
32
- secondary_markers: {
33
- data: 'd',
34
- meta: 'm'
35
- }
36
- })
37
-
38
- def initialize(user_id, namespace = nil, optional_key_config = {})
39
- optional_key_config.merge!(namespace: namespace) if namespace
40
- self.config = KEY_CONFIG.dup.merge!(optional_key_config)
41
-
42
- self.user_id = user_id
43
- self.short_id = config.primary[user_id]
44
-
45
- self.prefix = configure_prefix
46
-
47
- config.secondary_markers.each_pair do |type, character|
48
- self.class.send(:define_method, type) do
49
- instance_variable_get("@#{type}") || instance_variable_set("@#{type}", "#{prefix}#{config.separator}#{character}")
50
- end
51
- end
52
- end
53
-
54
- def keys
55
- config.secondary_markers.map { |k, v| [k, self.send(k)] }
56
- end
57
-
58
- def for(type)
59
- "#{prefix}#{config.separator}#{type.to_s}"
60
- end
61
-
62
- def to_s
63
- super.gsub(/SimpleFeed::Providers::Serialization/, '...*') + { user_id: user_id, short_id: short_id, keys: keys }.to_json
64
- end
65
-
66
- def inspect
67
-
68
- end
69
-
70
- private
71
-
72
- # eg. ff|u.123498
73
- def configure_prefix
74
- namespace = config.namespace ? "#{config.namespace}#{config.namespace_divider}" : ''
75
- "#{namespace}#{config.primary_marker}#{config.separator}#{short_id}"
76
- end
77
-
78
- end
79
- end
80
- end
81
- end
82
-