recapi 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/lib/recapi.rb +27 -2
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d12730fb7a1d92afff5bc3b2b2383830df795b24fada7104fd8e66c3ac84dc9a
4
- data.tar.gz: 5accff282fae359cb6fe664dbebf5957b351df0d155199ce04fa145049ffe3d4
3
+ metadata.gz: dfb3a3778d3f1733138f9f09c388edfd2e7cb946f6a2f38647b1070aa885d865
4
+ data.tar.gz: cc7d5eed12f41976d3e898a43bfee08364eaf2224b23f36b062b101577f7cb06
5
5
  SHA512:
6
- metadata.gz: eb5e428dd2fe97d7bdc35da5838b84bbcf68b1ece0ee137b8bbdd0dd4b75f24d0b3f30f01d392416a2e4034efa94ac8adc1aaceb570366b46fbf8b9012ea5646
7
- data.tar.gz: 71cc4af3ea8dd8b424c54e4cf69b94ec50c288fc0dbb7370797064d67cd9d40d06b56c46f94c8d92dd87a237653ef8a808ece2a0c523950ee994d14af2f2ba8b
6
+ metadata.gz: b48b41679b86910bb139237e630e5495bebfc4d1aa4fd9f924c2202344e5fbb903fb4c3966e5c608ffa406a90a2cb97c6a3fb75a989091509a41709f257e614f
7
+ data.tar.gz: b711bee7d27e579947e075e02a3e4487a97a2b7fe433e4d067d035d5b747157cd2cec7efa1adf34f6bd62bc1dd4d0e63ceb8f5612eb90bb929b1282a981f4051
data/README.md CHANGED
@@ -16,24 +16,28 @@ passes the shared vectors in [`spec/`](../../spec).
16
16
 
17
17
  ## Install
18
18
 
19
- Until the library is published as a gem, recapi serves it as one file (the
20
- dashboard's quick start shows the line with your address). In a Rails app,
21
- save it as `lib/recapi.rb`:
19
+ The library is the gem `recapi` on RubyGems (production releases publish it:
20
+ [docs/deployment.md](../../docs/deployment.md#publishing-the-ruby-library)):
21
+ add `gem "recapi"` to your Gemfile, or
22
22
 
23
23
  ```sh
24
- curl -fLo lib/recapi.rb https://api.<your-domain>/sdk/ruby/recapi-0.1.3.rb
24
+ bundle add recapi
25
25
  ```
26
26
 
27
- Once it is published on RubyGems (production releases publish it:
28
- [docs/deployment.md](../../docs/deployment.md#publishing-the-ruby-library)),
29
- `gem "recapi"` in your Gemfile, or `gem install recapi`, installs the same
30
- file as a gem, and `require "recapi"` loads it.
27
+ and `require "recapi"` loads it (Rails does it for you). Without access to
28
+ RubyGems, recapi serves the same file (the dashboard's quick start shows the
29
+ line with your address): save it as `lib/recapi.rb` and
30
+ `require Rails.root.join("lib/recapi").to_s` instead.
31
+
32
+ ```sh
33
+ curl -fLo lib/recapi.rb https://api.<your-domain>/sdk/ruby/recapi-0.1.4.rb
34
+ ```
31
35
 
32
36
  ## Quick start
33
37
 
34
38
  ```ruby
35
39
  # config/initializers/recapi.rb: one client per process, shared by its threads.
36
- require Rails.root.join("lib/recapi").to_s
40
+ require "recapi"
37
41
 
38
42
  # A secret key (rk_sec_…): it stays on your server.
39
43
  RECAPI = Recapi::Client.new(ENV.fetch("RECAPI_KEY"), "https://api.example.com",
@@ -78,7 +82,13 @@ max_batch:, logger:)` sets the options. The default `Recapi::MemoryStore`
78
82
  keeps the 10,000 most recent users in the process; pass `Rails.cache` (it
79
83
  has `read`/`write`/`delete`) or a Redis client (`get`/`set`/`del`) when
80
84
  several processes serve the same users. It stores the same JSON as the
81
- Python library, so both can share one store. Puma's threads share one
85
+ Python library, so both can share one store. Written to `Rails.cache` or a
86
+ redis-rb client, a user's state lasts 30 days from its last write, with no
87
+ setting to choose: well past the 24 hours a click earns credit, and recapi gives
88
+ a known `user_id` its history back afterwards. Any other store gets a plain
89
+ `set`/`write`, including a client wrapped in `ConnectionPool::Wrapper`: make it
90
+ expire entries itself after about 30 days, or Redis keeps them for ever. Puma's
91
+ threads share one
82
92
  client; a forked worker (Puma cluster, Unicorn, Passenger) starts its own
83
93
  queue and sender at its first call. Proxies come from `HTTPS_PROXY`,
84
94
  `HTTP_PROXY` and `NO_PROXY`, as `Net::HTTP` reads them. Failures are logged
data/lib/recapi.rb CHANGED
@@ -25,7 +25,7 @@ rescue LoadError
25
25
  end
26
26
 
27
27
  module Recapi
28
- VERSION = "0.1.3"
28
+ VERSION = "0.1.4"
29
29
 
30
30
  # Decayed user history, exactly as docs/history-spec.md (times in unix ms).
31
31
  # A port of sdk/python's history.py, snapshot merging included.
@@ -389,6 +389,15 @@ module Recapi
389
389
  # Shown items remembered per user.
390
390
  MAX_SHOWN_ITEMS = 100
391
391
  PUBLIC_KEY_PREFIX = "rk_pub_"
392
+ # How long a recognised shared store keeps a user's state after its last
393
+ # write. Far beyond the 24 hours of click credit, and by then a score has
394
+ # halved four times (under 5% is left). A known user_id gets its history
395
+ # back from recapi, which keeps 90 days, so only a visitor gone for a month
396
+ # starts afresh. It bounds a Redis at about one month's visitors instead of
397
+ # growing for ever.
398
+ STATE_LIFETIME_SECONDS = 30 * 24 * 60 * 60
399
+ # Where redis-rb defines set: 4.x and 5 on, older versions, Redis::Distributed.
400
+ REDIS_RB_SET = %w[Redis::Commands::Strings Redis Redis::Distributed].freeze
392
401
  # Open connections kept between calls: one per call in flight at the busiest moment.
393
402
  MAX_IDLE_CONNECTIONS = 16
394
403
  CODE_BY_STATUS = {
@@ -435,6 +444,7 @@ module Recapi
435
444
  end
436
445
 
437
446
  @store = store || MemoryStore.new
447
+ @store_lifetime = store_lifetime
438
448
  @timeout = timeout.to_f
439
449
  @interval = flush_interval.to_f
440
450
  @max_batch = max_batch.to_i.clamp(1, MAX_EVENTS_PER_REQUEST)
@@ -1010,11 +1020,26 @@ module Recapi
1010
1020
  @logger.error("recapi: store write failed: #{e.class}: #{e.message}")
1011
1021
  end
1012
1022
 
1023
+ # What makes this store's write expire the entry. By class name, since the
1024
+ # library requires neither gem. Any doubt (a subclass's own set, a wrapper
1025
+ # such as ConnectionPool::Wrapper) means no option, as for a store of the
1026
+ # customer's.
1027
+ def store_lifetime
1028
+ klass = @store.class
1029
+ if klass.public_method_defined?(:set)
1030
+ REDIS_RB_SET.include?(klass.instance_method(:set).owner.name) ? { ex: STATE_LIFETIME_SECONDS } : {}
1031
+ elsif klass.ancestors.any? { |ancestor| ancestor.name == "ActiveSupport::Cache::Store" }
1032
+ { expires_in: STATE_LIFETIME_SECONDS }
1033
+ else
1034
+ {}
1035
+ end
1036
+ end
1037
+
1013
1038
  # Redis clients have get/set/del, Rails.cache read/write/delete.
1014
1039
  def store_call(action, key, value = nil)
1015
1040
  case action
1016
1041
  when :get then @store.respond_to?(:get) ? @store.get(key) : @store.read(key)
1017
- when :set then @store.respond_to?(:set) ? @store.set(key, value) : @store.write(key, value)
1042
+ when :set then @store.respond_to?(:set) ? @store.set(key, value, **@store_lifetime) : @store.write(key, value, **@store_lifetime)
1018
1043
  else @store.respond_to?(:delete) ? @store.delete(key) : @store.del(key)
1019
1044
  end
1020
1045
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - recapi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-02 00:00:00.000000000 Z
11
+ date: 2026-11-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Ruby client for the recapi recommendation API: per-user history, event
14
14
  batching, retries, popular fallback. Standard library only.'