EventStore API
IChronicleClient
Section titled “IChronicleClient”Entry point for the library.
interface IChronicleClient : AutoCloseable { fun getEventStore( name: String, namespace: String = EventStoreNamespaceName.default.value ): EventStore
suspend fun getEventStores(): List<String> fun evictEventStores() fun dispose()}Construct it with ChronicleOptions:
// localhost:35000ChronicleClient(ChronicleOptions.development())
// any serverChronicleClient( ChronicleOptions.fromConnectionString("chronicle://chronicle.internal"))getEventStores lists every event store known to the kernel — not just the
ones this client has already opened via getEventStore. evictEventStores
clears this client’s internal cache of EventStore instances (and their
per-store subscriptions) without disposing the client itself — useful
between test classes that share a client instance.
IEventStore
Section titled “IEventStore”interface IEventStore { val name: String val namespace: String val eventLog: IEventLog val reactors: IReactorsService val reducers: IReducersService val projections: IProjectionsService val constraints: IConstraintsService val seeding: IEventSeedingService val readModels: IReadModelsService val unitOfWorkManager: UnitOfWorkManager val compliance: IComplianceService val eventTypes: IEventTypesService val namespaces: INamespacesService val externalServices: IExternalServicesService val jobs: IJobsService val eventStoreSubscriptions: IEventStoreSubscriptionsService val webhooks: IWebhooksService val identities: IIdentityManagerService val failedPartitions: IFailedPartitions
fun getEventSequence(id: EventSequenceId): IEventSequence}Every service is now on the interface itself — compliance, eventTypes,
namespaces, externalServices, jobs, eventStoreSubscriptions,
webhooks, and identities used to be available only on the concrete
EventStore class; code written against IEventStore (for example in
tests, behind a fake) now sees the full surface.
eventLog is the default event log sequence (EventSequenceId.eventLog).
Use getEventSequence(id) to get any other, non-default IEventSequence by
id — for example one used exclusively by a specific subsystem.
IEventSequence and IEventLog
Section titled “IEventSequence and IEventLog”IEventLog is IEventSequence plus a transactional entry point; both
share the same read/write surface.
interface IEventSequence { val id: EventSequenceId val appendOperations: SharedFlow<List<AppendedEventWithResult>>
suspend fun append( eventSourceId: String, event: Any, options: AppendOptions? = null ): AppendResult suspend fun appendMany( eventSourceId: String, events: List<Any>, options: AppendOptions? = null ): List<AppendResult> suspend fun appendMany( events: List<EventForEventSourceId>, concurrencyScopes: Map<String, ConcurrencyScope> = emptyMap(), correlationId: UUID? = null ): List<AppendResult> suspend fun hasEventsFor(eventSourceId: String): Boolean
suspend fun getTailSequenceNumber( eventSourceId: String? = null ): EventSequenceNumber suspend fun getForEventSourceIdAndEventTypes( eventSourceId: String, eventTypes: List<KClass<*>>, eventStreamType: String? = null, eventStreamId: String? = null, eventSourceType: String? = null ): List<AppendedEvent> suspend fun getFromSequenceNumber( sequenceNumber: EventSequenceNumber, eventSourceId: String? = null, eventTypes: List<KClass<*>>? = null ): List<AppendedEvent> suspend fun getNextSequenceNumber(): EventSequenceNumber suspend fun getTailSequenceNumberForObserver( observerType: KClass<*> ): EventSequenceNumber
suspend fun completeStream( eventStreamType: String, eventStreamId: String ): CompleteStreamResult
suspend fun redact( sequenceNumber: EventSequenceNumber, reason: RedactionReason ) suspend fun redactForEventSource( eventSourceId: String, reason: RedactionReason, eventTypes: List<KClass<*>> = emptyList() )}
interface IEventLog : IEventSequence { val transactional: ITransactionalEventSequence}Appending across event sources
Section titled “Appending across event sources”The eventSourceId overload of appendMany shapes every event in the
batch the same way. The List<EventForEventSourceId> overload instead lets each
event carry its own event source id and its own shaping, so one atomic
batch can span many event sources and many streams. concurrencyScopes is
keyed by event source id — only the sources present in the map are
concurrency checked, and any source left out is appended unchecked.
data class EventForEventSourceId( val eventSourceId: String, val event: Any, val eventStreamType: String? = null, val eventStreamId: String? = null, val eventSourceType: String? = null, val tags: List<String> = emptyList(), val occurred: Instant? = null, val subject: String? = null, val causation: List<Causation> = emptyList())Every unset field falls back to the same default a plain append uses,
resolved against that event’s own event source id.
Composed operations
Section titled “Composed operations”IEventSequenceOperations composes a batch that is decided in more than
one place, then commits it with a single perform(). Start one with the
operations() or forEventSourceId(...) extension on any
IEventSequence. Nothing reaches the kernel until perform() runs, and
getEventsToAppend() shows exactly what it will send.
interface IEventSequenceOperations { val eventSequence: IEventSequence
fun forEventSourceId( eventSourceId: String, configure: IEventSourceOperations.() -> Unit ): IEventSequenceOperations fun withCorrelationId(correlationId: UUID): IEventSequenceOperations fun withCausation(causation: List<Causation>): IEventSequenceOperations fun getAppendedEvents(): List<Any> fun getEventsToAppend(): List<EventForEventSourceId> fun clear() suspend fun perform(): List<AppendResult>}
interface IEventSourceOperations { val operations: List<IEventSequenceOperation> val concurrencyScope: ConcurrencyScope
fun withConcurrencyScope(concurrencyScope: ConcurrencyScope): IEventSourceOperations fun withConcurrencyScope( configure: ConcurrencyScopeBuilder.() -> Unit ): IEventSourceOperations fun append( event: Any, eventStreamType: String? = null, eventStreamId: String? = null, eventSourceType: String? = null, tags: List<String> = emptyList(), occurred: Instant? = null, subject: String? = null ): IEventSourceOperations fun <T : IEventSequenceOperation> getOperationsOfType(type: KClass<T>): List<T> fun getAppendedEvents(): List<Any>}Calling forEventSourceId twice for the same event source adds to what is
already staged rather than replacing it. The concurrency scope lives on the
event source, since that is where the kernel checks it: an event source
that never sets one is appended unchecked, and a scope already set is never
cleared by a later ConcurrencyScope.notSet — so a call that expresses no
expectation cannot silently disable a check that was explicitly asked for.
Causation is not composed here; it is derived from the ambient
CausationManager when perform() runs.
Java reaches this through EventSequenceOperationsJavaBridge
(operationsFor, forEventSourceId, perform) and
EventSourceOperationsJavaBridge (append, withConcurrencyScope), which
supply the blocking calls, Consumer-based configuration, and the optional
parameters Kotlin default arguments cannot give Java.
Reading events back
Section titled “Reading events back”getForEventSourceIdAndEventTypes gets events for one event source,
narrowed to specific event types and (optionally) a specific stream.
getFromSequenceNumber instead reads forward from a position in the
sequence, optionally narrowed by event source and/or event type —
useful for catching up from a bookmark. getTailSequenceNumber and
getNextSequenceNumber report the current end of the sequence (or of one
event source’s events within it) and the sequence number the next
appended event will receive, respectively. getTailSequenceNumberForObserver
reports the tail relative to the event types a specific reactor or reducer
type actually handles (discovered by reflection over its handler methods).
data class AppendedEvent( val context: EventContext, val content: String)content is the event’s raw stored JSON — deserialize it with the concrete
event class matching context.eventType to get a typed event object.
Redacting events
Section titled “Redacting events”redact permanently rewrites a single event’s content, identified by its
sequence number. redactForEventSource does the same for every event of a
given event source, optionally narrowed to specific event types (an empty
list, the default, redacts every event type for that source). Both are
destructive, irreversible content rewrites — not a soft delete or a field
mask. Once either call returns, the original content is gone from the
event store for good. Use them only for a confirmed compliance/erasure
request (e.g. GDPR “right to be forgotten”), typically alongside
IComplianceService.deleteEncryptionKey rather than
instead of it — deleting the encryption key is non-destructive to event
content (it just becomes unreadable), while redaction actually erases it.
@JvmInlinevalue class RedactionReason(val value: String)
sealed class CompleteStreamResult { data class Success( val sequenceNumber: EventSequenceNumber ) : CompleteStreamResult() data object DefaultStreamCannotBeCompleted : CompleteStreamResult() data object AlreadyCompleted : CompleteStreamResult()}Completing a stream
Section titled “Completing a stream”completeStream marks an event stream type/id pair as closed so no further
events can be appended to it, returning a CompleteStreamResult. The
default stream ("Default" paired with the default event stream id, the
one every plain append/appendMany call writes to) can never be
completed. Completing an already-completed stream returns
CompleteStreamResult.AlreadyCompleted rather than throwing.
Observing appends: appendOperations
Section titled “Observing appends: appendOperations”appendOperations is a hot SharedFlow that emits after every append made
through that specific IEventSequence instance completes, whether it
succeeded or failed — a single-event append emits a list of one element,
a batch appendMany emits the whole batch. It does not emit for
transactional appends made through ITransactionalEventSequence (those
only emit, as part of the underlying batch, once the unit of work commits
and the real append happens). Being a hot flow, only appends made after a
subscriber starts collecting are seen — nothing is replayed.
data class AppendedEventWithResult( val context: EventContext, val event: Any, val result: AppendResult)Concurrency control
Section titled “Concurrency control”Optimistic concurrency is opt-in per append, via
AppendOptions.concurrencyScope:
data class AppendOptions( val correlationId: UUID? = null, val concurrencyScope: ConcurrencyScope? = null, val eventSourceType: String? = null, val eventStreamType: String? = null, val eventStreamId: String? = null, val subject: String? = null, val tags: List<String> = emptyList(), val occurred: Instant? = null, val causation: List<Causation> = emptyList())
data class ConcurrencyScope( val sequenceNumber: EventSequenceNumber, val eventSourceId: Boolean = false, val eventStreamType: String? = null, val eventStreamId: String? = null, val eventSourceType: String? = null, val eventTypes: List<EventTypeDescriptor> = emptyList())Build one with ConcurrencyScopeBuilder rather than the constructor
directly — withSequenceNumber sets the expected position, and
withEventSourceId/withEventStreamType/withEventStreamId/
withEventSourceType/withEventType(s) each narrow which dimension the
check applies to. Leaving concurrencyScope unset (the default) is
equivalent to ConcurrencyScope.none — the append is not concurrency
checked. When the scope no longer matches, the append fails with
AppendResult.concurrencyViolation set instead of throwing.
Causation
Section titled “Causation”Every append carries a causation chain describing what led to it. By default
that chain is ambient: CausationManager builds one up per thread, and the
append reads it as it goes. Nearly every append should leave it at that.
Set AppendOptions.causation to attribute an append to a different chain —
an event imported from a legacy system, or a side effect that belongs to a
chain of its own rather than to the work the current thread happens to be
doing. An override deliberately leaves the ambient chain untouched, so later
appends are not attributed to something they had nothing to do with.
import io.cratis.chronicle.auditing.Causationimport io.cratis.chronicle.auditing.CausationTypeimport io.cratis.chronicle.eventSequences.AppendOptionsimport java.time.Instant
store.eventLog.append( "employee-1", EmployeeHired("Ada", "Lovelace", "Engineer"), AppendOptions( causation = listOf( Causation( Instant.parse("1998-06-01T09:00:00Z"), CausationType("LegacyImport"), mapOf("file" to "1998.csv") ) ) ))EventForEventSourceId carries the same field, which is how a reactor side
effect names its own chain. One caveat applies to batches: the kernel carries
a single chain per appendMany, not one per event, so a batch whose events
declare different chains throws CausationDiffersAcrossBatch rather than
silently keeping one of them. Give them the same causation, leave it unset,
or append them as separate batches. For a composed operation, set it once for
the whole batch with withCausation.
From Java, use AppendOptionsBuilder.causation(...).
AppendResult
Section titled “AppendResult”| Property | Type | Description |
|---|---|---|
isSuccess | Boolean | true when there are no violations or errors |
sequenceNumber | EventSequenceNumber | Position in the event log |
constraintViolations | List<ConstraintViolation> | On failure |
errors | List<AppendError> | On failure |
concurrencyViolation | ConcurrencyViolation? | Set on stale scope |
Unit of work
Section titled “Unit of work”IEventStore.unitOfWorkManager returns an IUnitOfWorkManager, which
creates and tracks IUnitOfWork instances scoped to the current thread.
interface IUnitOfWorkManager { val current: IUnitOfWork val hasCurrent: Boolean fun tryGetFor(correlationId: CorrelationId): IUnitOfWork? fun begin(): IUnitOfWork fun begin(correlationId: CorrelationId): IUnitOfWork fun setCurrent(unitOfWork: IUnitOfWork)}
interface IUnitOfWork { val isCompleted: Boolean val correlationId: CorrelationId val isSuccess: Boolean
fun addEvent( eventSequenceId: EventSequenceId, eventSourceId: String, event: Any, options: AppendOptions? = null ) fun getEvents(): List<Any> fun getConstraintViolations(): List<ConstraintViolation> fun getConcurrencyViolations(): List<ConcurrencyViolation> fun getAppendErrors(): List<AppendError>
suspend fun commit() suspend fun rollback()
fun onCompleted(callback: (IUnitOfWork) -> Unit) fun tryGetLastCommittedEventSequenceNumber(): EventSequenceNumber?}current throws NoUnitOfWorkHasBeenStarted if begin hasn’t been called
on this thread — check hasCurrent first if that’s expected. Prefer
staging events through store.eventLog.transactional.append/appendMany
(see Transactions) over calling addEvent
directly; the transactional event sequence resolves the right
EventSequenceId for you.
After commit(), isSuccess reflects whether every staged event was
appended without a constraint violation, concurrency violation, or append
error; getConstraintViolations()/getConcurrencyViolations()/
getAppendErrors() report the specifics, and
tryGetLastCommittedEventSequenceNumber() returns the highest sequence
number actually committed (null if nothing committed). onCompleted
registers a callback invoked once, whether the unit of work ends via
commit() or rollback() — it can be called multiple times to register
several callbacks.
IIdentityManagerService
Section titled “IIdentityManagerService”interface IIdentityManagerService { suspend fun rename(subject: String, name: String)}Renames the human-readable name the kernel has stored for the identity
identified by subject. This only updates the stored display name; it
does not change the Identity objects your process is currently using —
see
Correlation and identity.
INamespacesService
Section titled “INamespacesService”interface INamespacesService { suspend fun ensure(namespaceName: String) suspend fun getAll(): List<String>}ensure creates a namespace if it doesn’t already exist (idempotent).
getAll lists every namespace in the event store.
IReactorsService
Section titled “IReactorsService”interface IReactorsService { suspend fun register(reactor: Any): Job}A handler takes the event first, and after that anything the client can supply
for it — an EventContext, a read model resolved for the event’s event source,
or whatever an IReactorMethodArgumentResolver claims. Handlers may suspend.
IReactorMiddleware wraps every invocation. See
Artifact Registration for both.
Being told about a replay
Section titled “Being told about a replay”Implement ICanBeNotifiedAboutReplay and declare replayBegan and/or
replayEnded. Each takes a ReplayContext or nothing at all, and may suspend.
Chronicle replays each event source independently, so the notifications arrive
once per partition rather than once overall.
data class ReplayContext( val observerId: String, val partition: String, val sequenceNumber: EventSequenceNumber)The kernel flags the first and last event of a replay rather than sending a
separate signal, so replayBegan runs immediately before the first replayed
event is handled and replayEnded immediately after the last. A replay that
delivers no events produces no notification — there was no first event to flag.
Implementing the interface and declaring neither method is rejected at registration: a marker with nothing behind it would silently do nothing.
IFailedPartitions
Section titled “IFailedPartitions”A handler that throws stops the event source it threw on and leaves every other
one running. That keeps one bad event from halting the system, and it also means
a stuck partition announces itself nowhere. store.failedPartitions is how an
application finds out, and how an operator recovers once the cause is fixed.
interface IFailedPartitions { suspend fun getFor(observerId: String): List<FailedPartition> suspend fun getFor(observerClass: KClass<*>): List<FailedPartition> suspend fun retry( observerId: String, partition: String, eventSequenceId: EventSequenceId = EventSequenceId.eventLog ) suspend fun retry(observerClass: KClass<*>, partition: String)}
data class FailedPartition( val id: UUID, val observerId: String, val partition: String, val attempts: List<FailedPartitionAttempt>) { val lastAttempt: FailedPartitionAttempt?}
data class FailedPartitionAttempt( val occurred: Instant, val sequenceNumber: EventSequenceNumber, val messages: List<String>, val stackTrace: String)The overloads taking a KClass read the observer’s id — and, when retrying, the
sequence it observes — off the class exactly as registration does, so an observer
can be asked about by type rather than by remembering what its id came out as.
attempts is the history of the problem, oldest first: lastAttempt is the one
still standing in the way. Retrying an observer whose cause has not been fixed
simply adds another attempt, so fix first and retry after.
IReducersService
Section titled “IReducersService”interface IReducersService { suspend fun register(reducer: Any): Job}IReadModelsService
Section titled “IReadModelsService”interface IReadModelsService { suspend fun register(vararg readModelClasses: KClass<*>)
suspend fun <T : Any> getInstanceByKey( readModelClass: KClass<T>, key: String ): T?
suspend fun <T : Any> getInstances( readModelClass: KClass<T>, eventCount: Long? = null ): List<T>
suspend fun <T : Any> getSnapshotsById( readModelClass: KClass<T>, key: String ): List<ReadModelSnapshot<T>>
fun <T : Any> watch(readModelClass: KClass<T>): Flow<ReadModelChangeset<T>>
suspend fun dehydrateSession( readModelClass: KClass<*>, key: String, sessionId: String )
suspend fun <T : Any> release(instance: T, subject: String? = null): T suspend fun <T : Any> releaseMany(instances: List<T>): List<T>
val materialized: IMaterializedReadModels}getInstancesreplays events in-process to produce every instance of a read model, optionally capped to the firsteventCountevents.getSnapshotsByIdreturns the full history of intermediate states for one read model key, grouped by correlation id — eachReadModelSnapshotcarries the deserializedinstance, theeventsthat produced it, and (when known) when theyoccurredand theircorrelationId. UnlikegetInstanceByKey, which only returns the latest state, this is the way to inspect how a read model got to where it is.watchreturns aFlow<ReadModelChangeset<T>>of live changes, deserialized straight intoT— no polling, no manual JSON parsing. EachReadModelChangesetcarries themodelKey, thechangeType(Added/Modified/Removed), the deserializedreadModel(nullwhenremoved), and the triggering event’seventSequenceNumber/occurred/correlationIdwhen known.release/releaseManydecrypt@Pii-annotated properties on one or a batch of read model instances. The compliance subject defaults to each instance’sidproperty if present; passsubjectexplicitly toreleasewhen there is noidproperty or it isn’t the compliance subject.
IReadModelReactors
Section titled “IReadModelReactors”interface IReadModelReactors { fun register(reactor: IReadModelReactor): Job fun stop()}Construct the implementation with the read models to watch through and the
event log any side-effect events are appended to:
ReadModelReactors(store.readModels, store.eventLog). It is not reached
through IEventStore.
IReadModelReactoris a marker interface. Dispatch is by convention: a method namedadded,modifiedorremoved(matched case-insensitively) handles the correspondingReadModelChangeType. Its first parameter is the read model — a single instance or aListof them — and that type selects which read model is watched. An optional second parameter of typeReadModelChangesetcarries the change metadata.- A removal never carries an instance, so a Kotlin
removedhandler must declare its read model parameter nullable. Handlers that cannot be dispatched to are rejected at registration withInvalidHandlerSignature. - A handler may return an event, an
EventForEventSourceId, or aListof either, to be appended as a side effect — bare events use the changed instance’s key as the event source id. Return values that are not event types are ignored. registeris not a suspending function and returns theJobbacking that reactor’s subscriptions;stopcancels every reactor registered through the instance.
See the read model reactors guide for worked Kotlin and Java examples.
IProjectionsService
Section titled “IProjectionsService”interface IProjectionsService { suspend fun register(vararg projections: Any) suspend fun query( declaration: String, eventSequenceId: EventSequenceId = EventSequenceId.eventLog ): ProjectionQueryResult}Ad-hoc queries
Section titled “Ad-hoc queries”A registered projection is the right answer when a read model is part of the
system: the kernel keeps it up to date and the client hands you a type. query
is for the questions that are not part of the system — a one-off question of the
event log during an incident, a report nobody wants to deploy a projection for,
a declaration being written in an editor and tried out as it is typed.
Nothing is registered, nothing is persisted, and nothing observes afterwards. The kernel projects over the sequence and hands back the result.
val result = store.projections.query( """ from EmployeeHired set name to firstName """)
when (result) { is ProjectionQueryResult.Projected -> result.instancesOf(EmployeeName::class).forEach(::println) is ProjectionQueryResult.Invalid -> result.errors.forEach(::println)}A declaration is a piece of text, so the first thing that can go wrong is that
the kernel cannot parse it. That is an ordinary outcome of asking a question in
a language rather than an exception, so it comes back as Invalid carrying a
line and column per error — which is what you show whoever wrote it.
An ad-hoc declaration has no registered read model type, so entries is raw
JSON and instancesOf deserializes it into whatever shape the declaration
produced.
From Java, use ProjectionsServiceJavaBridge.query(...).
IConstraintsService
Section titled “IConstraintsService”interface IConstraintsService { suspend fun register(vararg constraints: Any)}IConstraintBuilder, passed into IConstraint.define, can additionally
scope every constraint added through it — see
Constraint scoping.
IEventTypesService
Section titled “IEventTypesService”interface IEventTypesService { suspend fun register(vararg eventClasses: KClass<*>) suspend fun registerSingle(eventClass: KClass<*>) suspend fun getAllGenerationsForEventType( eventTypeId: String ): List<Events.EventTypeRegistration> fun getRegisteredEventTypes(): List<EventTypeDescriptor>}register/registerSingle accept both plain @EventType-annotated
classes and IEventTypeMigration classes,
merging them into one registration per event type id, schema included
(generated from the latest generation’s class — see
Annotations). getRegisteredEventTypes returns every
event type registered through this instance so far; it’s what
eventStoreSubscriptions.subscribe falls back to when a subscription
isn’t narrowed to specific event types.
IEventSeedingService
Section titled “IEventSeedingService”interface IEventSeedingService { suspend fun seed(vararg seeders: Any)}See Seeding for IEventSeedingBuilder, including
namespace-scoped seed data via forNamespace.
IComplianceService
Section titled “IComplianceService”interface IComplianceService { suspend fun release( subject: String, schema: String, payload: String ): String suspend fun deleteEncryptionKey(identifier: String)}deleteEncryptionKey deletes the encryption key for a compliance subject —
a “right to be forgotten” erasure that leaves existing encrypted PII
content permanently undecryptable, without rewriting the events
themselves. Compare with redact/redactForEventSource,
which instead rewrites event content directly.
Java interop
Section titled “Java interop”Every service method above is a Kotlin suspend function, which Java cannot
call directly — a suspend fun carries a hidden continuation on the JVM.
Start here: the blocking client
Section titled “Start here: the blocking client”io.cratis.chronicle.java carries a blocking view of the client. It is what a
Java application should use, and it reads the same as the Kotlin one:
var client = BlockingChronicleClient.connect(ChronicleOptions.development());var eventStore = client.getEventStore("ChronicleConsole");
eventStore.getEventLog().append("some-event-source", new TestEvent("Hello world!"));Artifacts still register themselves, and the append waits for that to finish — so there is nothing to declare and nothing to sequence.
BlockingChronicleClient—connect,getEventStore(s),dispose. Closeable.BlockingEventStore— the event log and other sequences, transactional appends, read models, reactors, units of work, and registration.BlockingEventSequence—append,appendMany,hasEventsFor, sequence numbers, reading events back,redact.BlockingTransactionalEventSequence—appendandappendMany, staged against the current unit of work.BlockingReadModels—getInstanceByKeyandregister, by plainClass.BlockingReactors—register.BlockingUnitOfWork—commit,rollback. Closeable, rolling back unless it was committed.
Every call blocks until the kernel answers, which is what a main, a controller
method or a scheduled job wants. Do not call them from inside a coroutine —
Kotlin should use the suspending API directly. unwrap() on any of them returns
the suspending object underneath.
A unit of work reads as try-with-resources, so a throw needs no catch:
var eventStore = new BlockingEventStore(store);
try (var unitOfWork = eventStore.beginUnitOfWork()) { var transactional = eventStore.getTransactional();
transactional.append("order-123", new OrderPlaced(99.95)); transactional.append("inventory-widget", new InventoryReserved("widget", 1));
unitOfWork.commit();}The lower-level bridges
Section titled “The lower-level bridges”Underneath, the same package provides static bridges — each takes the service (or sequence) as its first argument. Reach for these for the corners the blocking client does not wrap.
A few Kotlin value classes (EventSequenceNumber, EventSequenceId,
EventTypeId, RedactionReason, CausationType) have mangled constructors on
the JVM ABI, so anything Java-facing takes and returns plain long/String
instead. Reading one back works — getValue() is the one accessor Kotlin leaves
unmangled — but Java can never construct one, which is why none appears in a
Java-facing signature. Causation.of(timestamp, type) exists for the same reason.
EventLogJavaBridge—append,appendMany,hasEventsFor,getForEventSourceIdAndEventTypes,getFromSequenceNumber,getTailSequenceNumber,getNextSequenceNumber,completeStream,redact,redactForEventSource,watchAppendOperationsTransactionalEventSequenceJavaBridge—append,appendManyConcurrencyScopeBuilderJavaBridge—withSequenceNumberEventTypesServiceJavaBridge—register,registerSingle,getAllGenerationsForEventTypeReadModelsJavaBridge—register,getInstanceByKey,getInstances,getSnapshotsById,watch,dehydrateSession,release,releaseMany,getMaterializedInstancesReactorsServiceJavaBridge—registerReducersServiceJavaBridge—registerProjectionsServiceJavaBridge—registerConstraintsServiceJavaBridge—registerEventSeedingServiceJavaBridge—seedEventSeedingBuilderJavaBridge/EventSeedingScopeBuilderJavaBridge—forEventTypeNamespacesServiceJavaBridge—ensure,getAllIdentityManagerServiceJavaBridge—renameUnitOfWorkJavaBridge—commit,rollbackComplianceServiceJavaBridge—release,deleteEncryptionKey
ReadModelReactors needs no bridge — its constructor takes plain types and
neither register nor stop is a suspending function, so Java calls both
directly.
import io.cratis.chronicle.java.EventLogJavaBridge;
var result = EventLogJavaBridge.append( store.getEventLog(), "emp-001", new EmployeeHired("emp-001", "Jane", "Smith", "Engineering"), null);EventSequenceNumber is a Kotlin value class, so an AppendResult has no
ordinary getter for it on the JVM. Read it with
EventLogJavaBridge.getSequenceNumber(result). The same applies to
getTailSequenceNumber/getNextSequenceNumber (they return long
directly) and to redact/redactForEventSource (they take a long
sequence number and a plain String reason rather than
EventSequenceNumber/RedactionReason).
ConstraintBuilderJavaBridge, UniqueConstraintBuilderJavaBridge,
ProjectionBuilderJavaBridge, and CausationManagerJavaBridge cover the
builder APIs that take a KClass in Kotlin, accepting a Class instead.