Class: AgentsControl::Store
- Inherits:
-
Object
- Object
- AgentsControl::Store
- Defined in:
- lib/agents_control/store.rb
Overview
State that survives a daemon restart.
Needed for two reasons at once:
1. Telegram limits button `callback_data` to 64 bytes. A session
identifier won't fit there, let alone an action's text — a button
carries only a short key, and the action itself lives here.
2. While the owner is out, the daemon can crash and come back up.
Pending questions have to survive that, or the agent is left
waiting forever.
There are dozens of records here, not millions, and only one process writes them — so an atomically-replaced file and a mutex, not a database.
Constant Summary collapse
- DEFAULT_TTL =
3600
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
Instance Method Summary collapse
-
#all ⇒ Object
All live records — for recovering state after a restart.
- #delete(key) ⇒ Object
- #get(key) ⇒ Object
-
#initialize(path: self.class.path, clock: -> { Time.now.to_i }) ⇒ Store
constructor
A new instance of Store.
- #prune ⇒ Object
-
#put(value, ttl: DEFAULT_TTL, key: nil) ⇒ Object
Short random key for a button.
-
#take(key) ⇒ Object
Take the value and delete it in the same step.
Constructor Details
#initialize(path: self.class.path, clock: -> { Time.now.to_i }) ⇒ Store
Returns a new instance of Store.
31 32 33 34 35 |
# File 'lib/agents_control/store.rb', line 31 def initialize(path: self.class.path, clock: -> { Time.now.to_i }) @path = path @clock = clock @mutex = Mutex.new end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
29 30 31 |
# File 'lib/agents_control/store.rb', line 29 def path @path end |
Class Method Details
.path ⇒ Object
24 25 26 27 |
# File 'lib/agents_control/store.rb', line 24 def self.path base = ENV["XDG_STATE_HOME"] || File.("~/.local/state") File.join(base, "agents_control", "store.json") end |
Instance Method Details
#all ⇒ Object
All live records — for recovering state after a restart.
79 80 81 82 |
# File 'lib/agents_control/store.rb', line 79 def all read.reject { |_key, entry| expired?(entry) } .transform_values { |entry| entry["value"] } end |
#delete(key) ⇒ Object
74 75 76 |
# File 'lib/agents_control/store.rb', line 74 def delete(key) write { |data| data.delete(key) } end |
#get(key) ⇒ Object
49 50 51 52 53 54 |
# File 'lib/agents_control/store.rb', line 49 def get(key) entry = read[key] return nil if entry.nil? || expired?(entry) entry["value"] end |
#prune ⇒ Object
84 85 86 |
# File 'lib/agents_control/store.rb', line 84 def prune write { |data| data.reject! { |_key, entry| expired?(entry) } } end |
#put(value, ttl: DEFAULT_TTL, key: nil) ⇒ Object
Short random key for a button. Eight base36 characters is ~41 bits, which is plenty: keys live minutes and are checked one at a time.
39 40 41 42 43 44 45 46 47 |
# File 'lib/agents_control/store.rb', line 39 def put(value, ttl: DEFAULT_TTL, key: nil) key ||= SecureRandom.alphanumeric(8).downcase write do |data| data[key] = { "value" => value, "expires_at" => now + ttl } end key end |
#take(key) ⇒ Object
Take the value and delete it in the same step.
This is exactly how a button press is handled: Telegram redelivers
the callback if the connection drops, and a finger can tap twice.
The first call gets the value, every call after gets nil, and the
action never runs a second time. The check and the delete happen
under one mutex, or two simultaneous presses could both see the value.
The value has to come back as the block's result, not via return:
an early return would unwind past the file write, the delete would
never reach disk, and the button would fire again after a restart.
66 67 68 69 70 71 72 |
# File 'lib/agents_control/store.rb', line 66 def take(key) write do |data| entry = data.delete(key) entry.nil? || expired?(entry) ? nil : entry["value"] end end |