packages feed

kiroku-store-0.12.0.0: src/Kiroku/Store/Append.hs

module Kiroku.Store.Append
  ( appendToStream,
    appendMultiStream,
  )
where

import Effectful (Eff, (:>))
import Effectful.Dispatch.Dynamic (send)
import GHC.Stack (HasCallStack)
import Kiroku.Store.Effect (Store (..))
import Kiroku.Store.Types

-- | Append a batch of events to a single stream under the given
-- 'ExpectedVersion' precondition.
--
-- When to use this vs.
-- 'Kiroku.Store.Transaction.runTransactionAppending':
--
-- * Reach for 'appendToStream' by default. It is the cheapest path —
--   one CTE on a pool connection, no Haskell-layer @BEGIN@/@COMMIT@
--   envelope, no caller-driven retry semantics to reason about.
-- * Reach for 'Kiroku.Store.Transaction.runTransactionAppending' /only/
--   when you need to atomically combine the append with additional SQL
--   writes that the store doesn't perform (most prominently a
--   projection-row insert from a downstream projection table). The
--   wrapper opens an explicit 'BEGIN'/'COMMIT', enables PostgreSQL
--   serialization-conflict retry of the entire body by default, and
--   threads the caller's continuation into the same transaction.
--
-- Semantics:
--
-- * If the precondition fails, the entire batch is rejected — appends are
--   all-or-nothing per call.
-- * On success, events are visible to subsequent reads on /this/ store
--   handle (read-your-own-writes is guaranteed). Other store handles see
--   the events after their pool connection's transaction-visibility
--   horizon advances.
-- * The returned 'AppendResult' carries the position of the /last/ event
--   in the batch.
--
-- Idempotent retries: supply 'Kiroku.Store.Types.EventData.eventId' yourself
-- and retry on transient failures. A retry whose previous attempt actually
-- committed surfaces as 'Kiroku.Store.Error.DuplicateEvent' (when the
-- events_pkey detail is parseable). A retry that observed
-- 'Kiroku.Store.Error.WrongExpectedVersion' on an 'ExactVersion' append
-- should be treated as ambiguous: either a concurrent writer raced you or
-- your previous attempt succeeded; the recovery in both cases is to
-- re-read the stream and decide.
--
-- Errors (all variants of 'Kiroku.Store.Error.StoreError'):
--
-- * 'Kiroku.Store.Error.WrongExpectedVersion' — 'ExactVersion' mismatch.
-- * 'Kiroku.Store.Error.StreamNotFound' — 'StreamExists' against a missing
--   or soft-deleted stream.
-- * 'Kiroku.Store.Error.ReservedStreamName' — the target is @$all@, which
--   is the global read stream and cannot be appended as an application
--   stream.
-- * 'Kiroku.Store.Error.StreamAlreadyExists' — 'NoStream' against an
--   existing stream (including soft-deleted).
-- * 'Kiroku.Store.Error.DuplicateEvent' — caller-supplied 'eventId'
--   collides with an existing event.
-- * 'Kiroku.Store.Error.EmptyAppendBatch' — the supplied event list is
--   empty.
--
-- Empty event lists: passing @[]@ is a programming mistake. The
-- interpreter rejects the call with 'Kiroku.Store.Error.EmptyAppendBatch'
-- before any database work. This avoids taking the global @$all@ row lock,
-- firing notifications, or creating an empty stream under 'NoStream'.
appendToStream ::
  (HasCallStack, Store :> es) =>
  StreamName ->
  ExpectedVersion ->
  [EventData] ->
  Eff es AppendResult
appendToStream name expected events = send (AppendToStream name expected events)

-- | Atomically append events to multiple streams in a single
-- transaction.
--
-- Either every per-stream append succeeds or all of them roll back —
-- there is no partial-commit state. The interpreter pre-locks the named
-- streams in deterministic @stream_id@ order to avoid deadlocks when two
-- concurrent multi-stream calls touch overlapping streams in different
-- user orders.
--
-- @$all@ is reserved for the global read stream. If any operation targets
-- @$all@, the whole call is rejected before opening the transaction with
-- 'Kiroku.Store.Error.ReservedStreamName'.
--
-- Per-stream errors are attributed to the stream that caused them: the
-- @Right results@ branch of the interpreter iterates the input list and
-- maps each empty CTE result back to the corresponding @(StreamName,
-- ExpectedVersion)@. Errors raised as PostgreSQL exceptions are
-- attributed via 'Kiroku.Store.Error.attributeMultiStreamError', which
-- parses the server-error detail for the offending stream name when
-- possible; the latent misattribution path (a future schema change that
-- introduces a constraint violation visible to the client) is covered
-- defensively.
--
-- The returned list mirrors the input order. Each 'AppendResult' carries
-- the corresponding stream's final state.
--
-- Empty operation input @[]@ returns @[]@ without a database round trip.
-- Any per-stream empty event list is rejected with
-- 'Kiroku.Store.Error.EmptyAppendBatch' before the transaction opens.
appendMultiStream ::
  (HasCallStack, Store :> es) =>
  [(StreamName, ExpectedVersion, [EventData])] ->
  Eff es [AppendResult]
appendMultiStream ops = send (AppendMultiStream ops)