Changelog of @hackage/kiroku-store 0.1.0.0

Changelog

0.1.0.0 — 2026-05-23

Changed — migrations own schema lifecycle

  • kiroku-store no longer embeds schema SQL or runs DDL during withStore. Apply kiroku-store-migrations before opening the store. This removes the duplicated bootstrap path between the runtime package and the migration package.
  • Removed Kiroku.Store.Schema, SchemaInitialization, InitializeSchemaOnAcquire, SkipSchemaInitialization, and ConnectionSettingsM.schemaInitialization.

Added — lookupStreamNames / lookupStreamName (plan 36)

  • Kiroku.Store.Read.lookupStreamNames :: [StreamId] -> Eff es (Map StreamId StreamName) resolves a batch of surrogate stream ids to their names in a single round trip (and lookupStreamName :: StreamId -> Eff es (Maybe StreamName) for one). This is the inverse of lookupStreamId and the supported way to recover the source stream name from a RecordedEvent's originalStreamId on fan-in reads — $all, categories, causation/correlation queries, and subscriptions — which carry only the surrogate id.

  • Chosen over carrying a originalStreamName field on every RecordedEvent: benchmarking showed returning the name on every read row costs ~13% on $all 100-event pages (decoding ~100 extra text values per page), whether the name came from a join or a denormalized column. The lookup API keeps the read hot path at its prior latency and makes consumers pay only when, and only as much as, they actually need names (one round trip per batch for the distinct ids). See docs/plans/36-add-originalstreamname-to-recordedevent.md.

  • A fresh installation now creates and uses a dedicated kiroku PostgreSQL schema instead of installing into public. The kiroku-store-migrations bootstrap creates the schema and sets search_path first, and every pooled connection runs SET search_path TO "<schema>", pg_catalog before any statement.

  • defaultConnectionSettings now defaults schema = "kiroku" (was "public"). The schema field is now authoritative for object location, table resolution, and the LISTEN <schema>.events channel — previously it only named the notification channel while table resolution depended on the connection-string search_path.

  • The kiroku-store-migrations codd bootstrap installs into kiroku; run it with CODD_SCHEMAS=kiroku. The runtime role needs privileges on the kiroku schema rather than on public.

Added — consumer groups for partitioned subscriptions (MasterPlan 4, plans 28–31)

  • Static, hash-partitioned consumer groups: a named subscription can be split across N members, each processing a disjoint, per-stream-ordered slice in parallel, with per-member checkpoints. Works for Category and $all targets.
  • Kiroku.Store.Subscription.Types (re-exported from Kiroku.Store):
    • ConsumerGroup { member :: Int32, size :: Int32 } — static membership descriptor. A stream is assigned to member (((hashtextextended(stream_id::text, 0) % size) + size) % size).
    • Two new fields on SubscriptionConfigM m: consumerGroup :: Maybe ConsumerGroup (default Nothing) and consumerGroupGuard :: Bool (default False, an opt-in startup advisory-lock conflict probe). defaultSubscriptionConfig sets both defaults, so existing callers compile unchanged.
    • Exceptions InvalidConsumerGroup (thrown by subscribe when size < 1 or member is out of range) and ConsumerGroupGuardConflict (thrown at startup when the guard detects a duplicate member).
  • subscriptions table gains consumer_group_member and consumer_group_size columns; the checkpoint key becomes the composite unique (subscription_name, consumer_group_member) (index ix_subscriptions_name_member). The change is applied by the migration package.
  • The four KirokuEventSubscription* observability events carry a trailing SubscriptionGroupContext (NonGroup | GroupMember member size), re-exported from Kiroku.Store.
  • Surfaced through every subscription entry point: the MonadIO subscribe/withSubscription, the effectful Subscription effect, the Streamly subscriptionStream bridge, and the Shibuya adapter (KirokuAdapterConfig gains consumerGroup :: Maybe ConsumerGroup).
  • New user guide docs/user/consumer-groups.md and a runnable example, cabal run kiroku-store:kiroku-consumer-group-example.

Added — causation chain and correlation walkers (plan 14)

  • Kiroku.Store.Causation (new module, re-exported from Kiroku.Store):
    • findCausationDescendants :: EventId -> Eff es (Vector RecordedEvent) — walk the causation graph forward from a seed event. Returns the seed plus every event whose causation_id chain leads back to it, in ascending global_position order. Backed by the existing partial index ix_events_causation_id, so cost scales as O(depth · log n) against the total event count.
    • findCausationAncestors :: EventId -> Eff es (Vector RecordedEvent) — symmetric walk in the other direction: returns the seed plus every ancestor reachable by following causation_id upward, in leaf-first depth order. Uses the same index.
    • findByCorrelation :: UUID -> Eff es (Vector RecordedEvent) — return every event whose correlation_id equals the input, in ascending global_position order. Backed by ix_events_correlation_id.
  • Kiroku.Store.Effect.Store gains a single new constructor FindEvents :: EventFilter -> Store m (Vector RecordedEvent) whose argument is a closed sum (Kiroku.Store.Types.EventFilterFilterCorrelation, FilterCausationDescendants, FilterCausationAncestors). The closed-sum shape preserves exhaustiveness checking for downstream mock interpreters.
  • The new reads honor StoreSettings.decodeHook, so any interpreter-level decode customization wired through ConnectionSettings applies on parity with readStreamForward / readAllForward / readCategory.

Added — interpreter-level event-data hooks (plan 13)

  • Kiroku.Store.Settings (new module, re-exported from Kiroku.Store):
    • StoreSettings { enrichEvent :: Maybe (EventData -> IO EventData), decodeHook :: Maybe (RecordedEvent -> IO RecordedEvent) } — optional interpreter-level hooks. enrichEvent runs on the append path before encoding (on the typed EventData the caller supplied); decodeHook runs on the read and subscription paths after decoding (on the typed RecordedEvent about to be surfaced). Both default to Nothing, taking a pure fast path that adds no traversal or allocation when the hook is absent.
    • defaultStoreSettings — both fields Nothing (no-op).
    • enrichEvents :: StoreSettings -> [EventData] -> IO [EventData] and decodeEvents :: StoreSettings -> Vector RecordedEvent -> IO (Vector RecordedEvent) — internal helpers reused by the interpreter, the subscription publisher, the subscription worker, and the new enrichEventsIO convenience.
  • Kiroku.Store.Connection.ConnectionSettings and Kiroku.Store.Connection.KirokuStore gain a storeSettings :: StoreSettings field. defaultConnectionSettings seeds it with defaultStoreSettings so existing callers see no behaviour change. withStore copies the value onto the runtime handle for the interpreter, the publisher, and the worker to read.
  • Kiroku.Store.Transaction:
    • runTransactionAppendingResource / runTransactionAppendingResourceNoRetry — hook-aware variants of runTransactionAppending / runTransactionAppendingNoRetry for callers running under a KirokuStoreResource effect. They apply enrichEvent to every EventData before opening the transaction.
    • enrichEventsIO :: KirokuStore -> [EventData] -> IO [EventData] — public convenience for direct callers of appendToStreamTx, who bypass the interpreter.

A typical use case is enriching every appended event with an OpenTelemetry trace context:

storeSettings = defaultStoreSettings
  { enrichEvent = Just $ \ed -> do
      ctx <- captureCurrentSpan
      pure (ed & #metadata %~ injectTraceContext ctx)
  }

Wired through ConnectionSettings's storeSettings field, the hook fires uniformly across appendToStream, appendMultiStream, runTransactionAppendingResource, every read path, and the subscription pipeline (live + catch-up).

Added — streaming single-stream forward read

  • Kiroku.Store.Read.readStreamForwardStream (re-exported from Kiroku.Store): a Streamly Stream (Eff es) RecordedEvent companion to readStreamForward. Internally paginates readStreamForward at a caller-supplied page size and yields events one at a time, enabling constant-memory folds over long streams. Shares SQL path and error semantics with readStreamForward.

Added — single-stream RunTransaction combinator (plan 11)

  • Kiroku.Store.Transaction (new module, re-exported from Kiroku.Store):
    • runTransaction / runTransactionNoRetry — bare escape hatch for running an arbitrary Hasql.Transaction.Transaction a against the store's connection pool. The default variant retries the body on PostgreSQL serialization conflicts; the -NoRetry variant runs exactly once. Both execute at ReadCommitted isolation in Write mode.
    • appendToStreamTx :: StreamName -> ExpectedVersion -> [PreparedEvent] -> UTCTime -> Tx.Transaction (Either AppendConflict AppendResult) — the Tx-flavored single-stream append building block. Useful inside a runTransaction block when the caller needs full control over conflict handling. Does not enforce reserved-stream rejection; pair with prepareEventsIO for UUIDv7 / createdAt prep.
    • runTransactionAppending / runTransactionAppendingNoRetry — recommended high-level wrapper for the keiro use case. Atomically composes a single-stream append with a caller-supplied Tx.Transaction continuation. Rejects $all up front, Tx.condemns on append conflicts (the continuation never runs), and surfaces both conflict and connection failures through the Either StoreError return type. Carries IOE :> es in addition to Store :> es.
  • Kiroku.Store.Effect:
    • New Store constructors RunTransaction and RunTransactionNoRetry. Mock interpreters are expected to reject these.
    • Internal building blocks PreparedEvent, prepareEvents, buildAppendParams, and appendDispatchTx are now exported (under the $internal haddock group) to support Kiroku.Store.Transaction. Not part of the supported public surface.
  • Kiroku.Store.Error:
    • AppendConflict — sum of append-precondition failures observable inside a Tx.Transaction body (WrongExpectedVersionConflict / StreamNotFoundConflict / StreamAlreadyExistsConflict). 1:1 with the corresponding StoreError constructors.
    • appendConflictToStoreError, emptyResultConflict — supporting pure helpers.

Changed

  • appendMultiStream's interpreter now dispatches per-stream appends through the shared appendDispatchTx helper. Behavior is unchanged.
  • appendToStream's haddock now describes when to reach for runTransactionAppending instead.