Module: ActsAsTbackend::Fact

Defined in:
lib/acts_as_tbackend/fact.rb

Overview

Builds facts with a deterministic, domain-derived id so a retry re-sends the same id and collapses to an idempotent replay instead of a duplicate.

NEVER put wall-clock in the id. Derive it from the source record's own version stamp (e.g. updated_at), which is stable across retries and advances on every real edit. The observation wall-clock belongs in transaction_time only.

id = ActsAsTbackend::Fact.derive_id(
store: "orders", record_id: order.id, event_type: "order.accepted",
source_version: order.updated_at)
fact = ActsAsTbackend::Fact.build(
id:, store: "orders", key: "order:#{order.id}", value: {...},
valid_time: order.scheduled_at)

Class Method Summary collapse

Class Method Details

.build(id:, store:, key:, value:, valid_time: nil, transaction_time: nil, causation: nil, schema_version: 1, producer: nil) ⇒ Object

Builds a fact envelope. value_hash is intentionally omitted: the daemon stamps its own canonical hash on write_fact_once and is the authority.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/acts_as_tbackend/fact.rb', line 40

def build(id:, store:, key:, value:, valid_time: nil, transaction_time: nil,
          causation: nil, schema_version: 1, producer: nil)
  fact = {
    "id" => id,
    "store" => store.to_s,
    "key" => key.to_s,
    "value" => value,
    "transaction_time" => (transaction_time || now).to_f,
    "schema_version" => schema_version
  }
  fact["valid_time"] = valid_time.to_f unless valid_time.nil?
  fact["causation"] = causation unless causation.nil?
  fact["producer"] = producer unless producer.nil?
  fact
end

.derive_id(store:, record_id:, event_type:, source_version:) ⇒ Object

Deterministic occurrence id. Components must be colon-free (the ":" is the id separator); source_version is normalised to a colon-free token.



24
25
26
# File 'lib/acts_as_tbackend/fact.rb', line 24

def derive_id(store:, record_id:, event_type:, source_version:)
  "#{store}:#{record_id}:#{event_type}:#{version_token(source_version)}"
end

.nowObject



56
57
58
# File 'lib/acts_as_tbackend/fact.rb', line 56

def now
  Process.clock_gettime(Process::CLOCK_REALTIME)
end

.version_token(source_version) ⇒ Object

A Time is encoded as an integer microsecond epoch — fully deterministic (no float formatting) and colon-free. Anything else is used as-is (stringified).



30
31
32
33
34
35
36
# File 'lib/acts_as_tbackend/fact.rb', line 30

def version_token(source_version)
  if source_version.respond_to?(:usec) && source_version.respond_to?(:to_i)
    source_version.to_i * 1_000_000 + source_version.usec
  else
    source_version.to_s
  end
end