class OpenUI::Rails::Actions::Idempotency::MemoryStore

Default, single-process idempotency store. Hosts that run multiple processes should inject a shared store (e.g. Redis or the database) that implements the same contract.

Contract:

claim(key, ttl:, digest:) -> [status, result, digest, claim_token]
status is :claimed, :pending or :replay (even for a nil result).
record(key, result, claim_token:) ignores an expired/replaced claim.

Public Class Methods

new(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } click to toggle source
# File lib/openui/rails/actions/idempotency/memory_store.rb, line 18
def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
  @entries = {}
  @mutex = Mutex.new
  @clock = clock
end

Public Instance Methods

claim(key, ttl:, digest: nil) click to toggle source
# File lib/openui/rails/actions/idempotency/memory_store.rb, line 24
def claim(key, ttl:, digest: nil)
  raise ConfigurationError, "idempotency ttl must be positive" unless ttl.is_a?(Numeric) && ttl.finite? && ttl > 0

  @mutex.synchronize do
    now = @clock.call
    purge(now)

    if (entry = @entries[key])
      return [ entry[:completed] ? :replay : :pending, entry[:result], entry[:digest], entry[:token] ]
    end

    token = SecureRandom.hex(16)
    @entries[key] = { digest: digest, result: nil, completed: false, token: token, expires_at: now + ttl }
    [ :claimed, nil, digest, token ]
  end
end
record(key, result, claim_token:) click to toggle source
# File lib/openui/rails/actions/idempotency/memory_store.rb, line 41
def record(key, result, claim_token:)
  @mutex.synchronize do
    entry = @entries[key]
    if entry && entry[:token] == claim_token && entry[:expires_at] > @clock.call
      entry[:result] = result
      entry[:completed] = true
    end
  end
end
size() click to toggle source
# File lib/openui/rails/actions/idempotency/memory_store.rb, line 51
def size
  @mutex.synchronize { @entries.size }
end