pub fn append_and_apply_event(
paths: &RunPaths,
kind: &str,
node_id: Option<&NodeId>,
idempotency_key: Option<&str>,
data: Value,
) -> Result<AppendResult>Expand description
The one canonical mutation entry point: append a single event to
events.jsonl and fold it into the projection files via the reducer,
all under the run’s flock, with idempotency-key dedup.
On success, every events.jsonl line is folded into manifest.json /
nodes/*.json / discussions/*.json / spinoffs/*.json before the lock
is released, so a read CLI run a millisecond later never sees a stale
projection. This is not a crash-atomic transaction: the event is fsynced
before the reducer runs, so a crash (or an I/O error from apply_event)
after the append but before the projection write leaves the log ahead of
the projections — recoverable only by a future rebuild_projections. The
log is the source of truth; projections are a derived cache.
The append is transactional against reducer validation: the event is
first reduced through reduce_event_to_ops under the
lock — the single plan-then-commit path that both validates and computes
the projection writes — and only a validating event is appended (and
fsynced) and then committed by the reducer. A reducer-rejected event (a
CorruptEventLog for a malformed payload) errors before any bytes are
written, so the log never gains a poison line that a future replay /
rebuild_projections would choke on.
(A pre-existing torn tail may still be truncated before validation runs —
those bytes are uncommitted by definition; see recover_last_seq.)
When idempotency_key is Some and a prior event with the same kind +
key already exists (find_prior_with_key), nothing is appended or
applied: the result carries the prior event’s seq, idempotent_replay: true, and prior: Some(..) so the caller can detect a key reused with a
conflicting payload. With idempotency_key: None no scan runs.
Callers that must compose several writes — or a read-modify-write
transaction (read a projection, decide, then append) — under one lock
window hold the lock themselves and use append_and_apply_unlocked,
the sanctioned lock-held composition path. Re-entering this function
while already holding the lock would deadlock: flock blocks when a
second open of the lock file from the same process tries LOCK_EX.