Module: Clickwrap::CurrentState
- Defined in:
- lib/clickwrap/current_state.rb
Overview
Projects retained event payloads onto the current-state table.
Everything here is derived from retained event payloads. rebuild_for!
replays them while they still contain the affected statement identity. A
reviewed core disposition deliberately removes that identity, so rebuilding
through a disposed root would invent or silently lose state. The method
refuses first and leaves the existing projection untouched in that case.
Constant Summary collapse
- INITIAL_EVENT_TYPES =
imported_legacyprojects like a capture: the source system says the act happened, and a migrated application must keep answering "did this person agree?" the way it answered the day before the migration — otherwise every import ends in a mass forced re-acceptance, which is exactly the history-rewriting an import exists to avoid. What stays different is the evidence, not the answer: the event keepsimported_providerattribution, its receipt names every unknown, andrequire_current_versionstill sends people back when the documents move on. (An exemption, by contrast, records that NO human acted — it projects under its own state and never satisfies a human-action predicate.) (Vocabulary::HUMAN_ACTION_EVENT_TYPES + %w[exemption imported_legacy]).freeze
- TRANSITION_STATE_BY_EVENT_TYPE =
{ "withdrawal" => "withdrawn", "supersession" => "superseded", "expiry" => "expired", "consumption" => "consumed", "revocation" => "revoked" }.freeze
Class Method Summary collapse
-
.apply!(event) ⇒ Object
Applies one event to the projection.
- .apply_statement!(event, statement) ⇒ Object
-
.projects_into_current_state?(event) ⇒ Boolean
Whether this event is allowed to shape the projection.
-
.rebuild_for!(actor_reference:) ⇒ Object
Rebuilds the projection for one actor from retained event payloads.
-
.transition!(state, to:, event:, at: nil) ⇒ Object
Marks a statement's projection with a lifecycle outcome, without touching the event that produced it.
Class Method Details
.apply!(event) ⇒ Object
Applies one event to the projection. Runs inside the capture's transaction, so a failure here rolls the capture back rather than leaving evidence whose current state nobody can query.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/clickwrap/current_state.rb', line 36 def apply!(event) return event unless INITIAL_EVENT_TYPES.include?(event.event_type) # An import quarantined with `counts_as_current: false` must stay # quarantined through every path — including a projection REBUILD. # Without this guard, `rebuild_for!` would launder consent the host # explicitly declined to honour into an active grant. return event unless projects_into_current_state?(event) StatementIdentityLock.acquire_for_actor!(event.actor_reference) identities = event.statements.map { |statement| identity_for(event, statement) } identities.sort_by { |identity| identity.fetch(:identity_digest) }.each do |identity| StatementIdentityLock.acquire!(identity.fetch(:identity_digest)) end event.statements.each { |statement| apply_statement!(event, statement) } end |
.apply_statement!(event, statement) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/clickwrap/current_state.rb', line 53 def apply_statement!(event, statement) identity = identity_for(event, statement) loop do return StatementState.transaction(requires_new: true) do state = StatementState.find_or_initialize_by(identity) return state if candidate_is_not_newer?(state, event, statement) # A previous grant for the same identity is superseded rather # than overwritten. The projection moves on; the event that # recorded the earlier act stays exactly where it was. state.assign_attributes( kind: statement.kind, purpose_key: statement.purpose_key, actor_type: event.actor_type, actor_id: event.actor_id, subject_type: event.subject_type, subject_id: event.subject_id, subject_fingerprint: statement.subject_fingerprint, represented_party_reference: event.represented_party_reference.to_s, state: state_for(event, statement), current_action: statement.action, current_event_id: event.id, root_event_id: event.root_event_id || event.id, policy_revision_id: event.policy_revision_id, effective_at: effective_at_for(event, statement), expires_at: statement.expires_at, one_time: statement.one_time, document_version_ids: document_version_ids_for(event, statement) ) (state, event, statement) state.save! state end rescue ::ActiveRecord::RecordNotUnique # PostgreSQL marks a transaction failed after a uniqueness error. # The requires_new savepoint above absorbs that state before retry. end end |
.projects_into_current_state?(event) ⇒ Boolean
Whether this event is allowed to shape the projection. Captures and
every other initial type always do; an imported_legacy event carries
its own answer in the structured provenance it was written with.
Imports that predate the flag project (they were written with exactly
that intent); only an explicit counts_as_current: false quarantines.
145 146 147 148 149 |
# File 'lib/clickwrap/current_state.rb', line 145 def projects_into_current_state?(event) return true unless event.event_type == "imported_legacy" event.provider_verification.to_h["counts_as_current"] != false end |
.rebuild_for!(actor_reference:) ⇒ Object
Rebuilds the projection for one actor from retained event payloads. Refuses before deleting anything when an existing state depends on a disposed root whose statement identity can no longer be reconstructed.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/clickwrap/current_state.rb', line 125 def rebuild_for!(actor_reference:) StatementState.transaction do StatementIdentityLock.acquire_for_actor!(actor_reference) existing_states = StatementState.for_actor(actor_reference).lock.to_a ensure_rebuildable!(existing_states, actor_reference) StatementState.for_actor(actor_reference).delete_all Event.for_actor(actor_reference) .chronological .includes(:statements) .to_a .each { |event| replay!(event) } end end |
.transition!(state, to:, event:, at: nil) ⇒ Object
Marks a statement's projection with a lifecycle outcome, without touching the event that produced it.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/clickwrap/current_state.rb', line 96 def transition!(state, to:, event:, at: nil) at ||= Clickwrap.now StatementIdentityLock.acquire_for_actor!(state.actor_reference) StatementIdentityLock.acquire!(state.identity_digest) state = StatementState.lock.find(state.id) return state if event_is_not_newer?(state, event, effective_at: at) attributes = { state: to.to_s, current_event_id: event.id, current_action: event_action_for(to), effective_at: at } case to.to_s when "withdrawn" then attributes[:withdrawn_at] = at when "superseded" then attributes[:superseded_at] = at when "consumed" then attributes[:consumed_at] = at when "revoked" then attributes[:revoked_at] = at when "corrected" then attributes[:corrected_at] = at end state.update!(attributes) end |