Clients
Orisun exposes gRPC services. You can use the official typed clients, grpcurl, or generate bindings for another language from the protobuf files.
The examples on this page target client builds that include SaveEventsV2,
write IDs, and GetWriteContext.
Older client releases continue to work through the deprecated SaveEvents RPC,
but they cannot send more than one query-level observation. Upgrade the client
before migrating a command that depends on multiple independent reads.
Use the client repository revisions referenced by the current Orisun source checkout for these features. The package-manager commands below may install older published clients. Updating source revisions does not publish new packages.
Official Clients​
| Language | Package | Repository |
|---|---|---|
| Go | github.com/oexza/orisun-client-go | orisun-client-go |
| Node.js | @orisun/eventstore-client | orisun-node-client |
| Java | com.orisunlabs:orisun-java-client | orisun-client-java |
Install​
- Go
- Node.js
- Java
go get github.com/oexza/orisun-client-go
The package is currently installed from GitHub (npm publication is pending):
npm install github:OrisunLabs/orisun-node-client
Or in package.json:
{
"dependencies": {
"@orisun/eventstore-client": "github:OrisunLabs/orisun-node-client"
}
}
Published to GitHub Packages. Add the repository and dependency, then authenticate with a GitHub personal access token.
Maven (pom.xml):
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/OrisunLabs/orisun-client-java</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.orisunlabs</groupId>
<artifactId>orisun-java-client</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
Gradle (build.gradle):
repositories {
maven {
url = 'https://maven.pkg.github.com/OrisunLabs/orisun-client-java'
credentials {
username = System.getenv('GITHUB_USERNAME')
password = System.getenv('GITHUB_TOKEN')
}
}
}
dependencies {
implementation 'com.orisunlabs:orisun-java-client:0.0.1'
}
Use the typed clients when you want request/response objects and subscription helpers. Use grpcurl for operational checks, debugging, and quick experiments.
Boundary prerequisite​
Client event operations target an active boundary; a boundary name is not
created implicitly by the first write. Before starting an application, define
the storage with Admin/CreateBoundary—setting existed_before_catalog when
adopting existing physical storage—then wait for Admin/GetBoundary to report
BOUNDARY_LIFECYCLE_STATUS_ACTIVE. See the
Admin boundary API for placements, lifecycle
states, and errors.
The Node client exposes typed helpers:
import { AdminClient, BoundaryStatus } from '@orisun/eventstore-client';
const admin = new AdminClient({
host: 'localhost',
port: 5005,
username: 'admin',
password: 'changeit',
});
const {boundary: definition} = await admin.createBoundary({
name: 'accounts',
description: 'Account lifecycle events',
placement: {backend: 'postgres', namespace: 'public'},
});
let boundary = definition;
while (boundary.status === BoundaryStatus.PROVISIONING) {
await new Promise(resolve => setTimeout(resolve, 100));
({boundary} = await admin.getBoundary('accounts'));
}
if (boundary.status !== BoundaryStatus.ACTIVE) {
throw new Error(`Boundary provisioning failed: ${boundary.lastError}`);
}
const {boundaries} = await admin.listBoundaries();
Set existedBeforeCatalog: true on createBoundary(...) when the physical
storage already exists. The command returns a definition in PROVISIONING; do
not treat the response as readiness. In clustered deployments, retry an EventStore
request that briefly returns FAILED_PRECONDITION after the shared catalog
becomes ACTIVE; the selected node is still completing its local runtime
installation.
First program​
A complete command loop: connect, save an event with a Command Context Consistency check, read the latest carried state from one snapshot, handle a conflict, and subscribe a projector. The examples assume the accounts boundary has already reached ACTIVE. Pick your language once; the same groupId carries over to the EventStore API and Tutorial pages.
- Go
- Node.js
- Java
- grpcurl
package main
import (
"context"
"errors"
"log"
orisun "github.com/oexza/orisun-client-go"
eventstore "github.com/oexza/orisun-client-go/eventstore"
)
func main() {
client, err := orisun.New(
"localhost:5005",
orisun.WithCredentials("admin", "changeit"),
orisun.WithInsecure(),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
accountOpenedID := "018f2d5e-0001-7000-8000-000000000001"
accountRootQuery := &eventstore.Query{
Criteria: []*eventstore.Criterion{{
Tags: []*eventstore.Tag{
{Key: "__eventType", Value: "AccountOpened"},
{Key: "accountOpenedId", Value: accountOpenedID},
},
}},
}
accountContextQuery := &eventstore.Query{
Criteria: []*eventstore.Criterion{
accountRootQuery.Criteria[0],
{Tags: []*eventstore.Tag{{Key: "scopes.accountOpenedId", Value: accountOpenedID}}},
},
}
// 1. Open the account (context must be empty).
_, err = client.SaveEventsV2(ctx, &eventstore.SaveEventsV2Request{
Boundary: "accounts",
Consistency: []*eventstore.ConsistencyObservation{{
Query: accountRootQuery,
Position: &eventstore.Position{CommitPosition: -1, PreparePosition: -1},
}},
Events: []*eventstore.EventToSave{{
EventId: accountOpenedID, EventType: "AccountOpened",
Data: `{"accountOpenedId":"018f2d5e-0001-7000-8000-000000000001","balance":0}`,
}},
})
if err != nil {
var conflict *orisun.OptimisticConcurrencyException
if errors.As(err, &conflict) {
log.Printf("conflict: expected=%v actual=%v", conflict.ExpectedVersion(), conflict.ActualVersion())
}
log.Fatal(err)
}
// 2. Read the latest carried state and construct its V2 observation.
latest, err := client.GetLatestByCriteria(ctx, &eventstore.GetLatestByCriteriaRequest{
Boundary: "accounts",
Criteria: accountContextQuery.Criteria,
})
if err != nil {
log.Fatal(err)
}
observed := &eventstore.ConsistencyObservation{
Query: accountContextQuery,
Position: latest.ContextPosition,
}
// 3. Save a decision only while that exact context is current.
write, err := client.SaveEventsV2(ctx, &eventstore.SaveEventsV2Request{
Boundary: "accounts",
Consistency: []*eventstore.ConsistencyObservation{observed},
Events: []*eventstore.EventToSave{{
EventId: "018f2d5e-0002-7000-8000-000000000002",
EventType: "AccountReviewed",
Data: `{"scopes.accountOpenedId":"018f2d5e-0001-7000-8000-000000000001"}`,
}},
})
if err != nil {
var conflict *orisun.OptimisticConcurrencyException
if errors.As(err, &conflict) {
// Re-read and re-decide. Do not retry this stale observation.
}
log.Fatal(err)
}
// 4. Subscribe a projector after the command just committed.
handler := orisun.NewSimpleEventHandler().
WithOnEvent(func(e *eventstore.Event) error { return nil }).
WithOnError(func(e error) { log.Printf("subscription: %v", e) })
sub, err := client.SubscribeToEvents(ctx, &eventstore.CatchUpSubscribeToEventStoreRequest{
Boundary: "accounts",
SubscriberName: "balance-projector",
AfterPosition: write.LogPosition,
}, handler)
if err != nil {
log.Fatal(err)
}
defer sub.Close()
}
import { EventStoreClient } from '@orisun/eventstore-client';
const client = new EventStoreClient({
host: 'localhost',
port: 5005,
username: 'admin',
password: 'changeit',
});
const accountOpenedId = '018f2d5e-0001-7000-8000-000000000001';
const accountRootQuery = {
criteria: [{
tags: [
{ key: '__eventType', value: 'AccountOpened' },
{ key: 'accountOpenedId', value: accountOpenedId },
],
}],
};
const accountContextQuery = {
criteria: [
accountRootQuery.criteria[0],
{ tags: [{ key: 'scopes.accountOpenedId', value: accountOpenedId }] },
],
};
// 1. Open the account (context must be empty).
try {
await client.saveEventsV2({
boundary: 'accounts',
consistency: [{
query: accountRootQuery,
position: { commitPosition: -1, preparePosition: -1 },
}],
events: [
{
eventId: accountOpenedId,
eventType: 'AccountOpened',
data: { accountOpenedId, balance: 0 },
},
],
});
} catch (error) {
if (error.message.includes('AlreadyExists')) {
// Concurrency conflict. Re-read the context and retry.
} else {
throw error;
}
}
// 2. Read the latest carried state and construct its V2 observation.
const latest = await client.getLatestByCriteria({
boundary: 'accounts',
criteria: accountContextQuery.criteria,
});
const balance = latest.results[1].event?.data.balanceAfter ?? latest.results[0].event?.data.balance ?? 0;
const observation = {
query: accountContextQuery,
position: latest.contextPosition,
};
// 3. Save a decision only while that exact context is current.
const write = await client.saveEventsV2({
boundary: 'accounts',
consistency: [observation],
events: [{
eventId: '018f2d5e-0002-7000-8000-000000000002',
eventType: 'AccountReviewed',
data: { 'scopes.accountOpenedId': accountOpenedId, balanceObserved: balance },
}],
});
// 4. Subscribe a projector after the command just committed.
const subscription = client.subscribeToEvents(
{ subscriberName: 'balance-projector', boundary: 'accounts', afterPosition: write.logPosition },
async (event) => { /* apply event, then checkpoint event.position */ },
(error) => console.error('subscription error:', error),
);
// subscription.cancel() to stop
import com.orisunlabs.orisun.client.OrisunClient;
import com.orisunlabs.orisun.client.OptimisticConcurrencyException;
import com.orisunlabs.orisun.client.EventSubscription;
import com.orisun.eventstore.Eventstore;
try (OrisunClient client = OrisunClient.newBuilder()
.withServer("localhost", 5005)
.withBasicAuth("admin", "changeit")
.build()) {
String accountOpenedId = "018f2d5e-0001-7000-8000-000000000001";
Eventstore.Criterion accountRoot = Eventstore.Criterion.newBuilder()
.addTags(Eventstore.Tag.newBuilder().setKey("__eventType").setValue("AccountOpened").build())
.addTags(Eventstore.Tag.newBuilder().setKey("accountOpenedId").setValue(accountOpenedId).build())
.build();
Eventstore.Query accountRootQuery = Eventstore.Query.newBuilder()
.addCriteria(accountRoot)
.build();
Eventstore.Query accountContextQuery = Eventstore.Query.newBuilder()
.addCriteria(accountRoot)
.addCriteria(Eventstore.Criterion.newBuilder()
.addTags(Eventstore.Tag.newBuilder()
.setKey("scopes.accountOpenedId").setValue(accountOpenedId).build())
.build())
.build();
// 1. Open the account (context must be empty).
try {
client.saveEventsV2(Eventstore.SaveEventsV2Request.newBuilder()
.setBoundary("accounts")
.addConsistency(Eventstore.ConsistencyObservation.newBuilder()
.setQuery(accountRootQuery)
.setPosition(Eventstore.Position.newBuilder()
.setCommitPosition(-1).setPreparePosition(-1)))
.addEvents(Eventstore.EventToSave.newBuilder()
.setEventId(accountOpenedId)
.setEventType("AccountOpened")
.setData("{\"accountOpenedId\":\"018f2d5e-0001-7000-8000-000000000001\",\"balance\":0}")
.build())
.build());
} catch (OptimisticConcurrencyException conflict) {
// Concurrency conflict. Re-read the context and retry.
}
// 2. Read the latest carried state and construct its V2 observation.
Eventstore.GetLatestByCriteriaResponse latest = client.getLatestByCriteria(
Eventstore.GetLatestByCriteriaRequest.newBuilder()
.setBoundary("accounts")
.addAllCriteria(accountContextQuery.getCriteriaList())
.build());
Eventstore.ConsistencyObservation observation = Eventstore.ConsistencyObservation.newBuilder()
.setQuery(accountContextQuery)
.setPosition(latest.getContextPosition())
.build();
// 3. Save a decision only while that exact context is current.
Eventstore.WriteResult write = client.saveEventsV2(
Eventstore.SaveEventsV2Request.newBuilder()
.setBoundary("accounts")
.addConsistency(observation)
.addEvents(Eventstore.EventToSave.newBuilder()
.setEventId("018f2d5e-0002-7000-8000-000000000002")
.setEventType("AccountReviewed")
.setData("{\"scopes.accountOpenedId\":\"018f2d5e-0001-7000-8000-000000000001\"}")
.build())
.build());
// 4. Subscribe a projector after the command just committed.
EventSubscription sub = client.subscribeToEvents(
Eventstore.CatchUpSubscribeToEventStoreRequest.newBuilder()
.setBoundary("accounts")
.setSubscriberName("balance-projector")
.setAfterPosition(write.getLogPosition())
.build(),
new EventSubscription.EventHandler() {
public void onEvent(Eventstore.Event event) { /* apply + checkpoint */ }
public void onError(Throwable error) { error.printStackTrace(); }
public void onCompleted() {}
});
sub.close();
}
No install beyond the grpcurl binary. See the Tutorial for the same loop with grpcurl, or the EventStore API for each RPC.
Write IDs and recorded consistency contexts​
Save results and returned events expose WriteId in Go, writeId in Node.js,
and getWriteId() in Java. Treat this value as opaque and retain its boundary.
It identifies the complete atomic write, including when a write saves several
events. Historical events without recorded contexts return an empty write ID.
Use the write ID returned by a save or read to retrieve the observations checked when that write committed:
- Go
- Node.js
- Java
writeContext, err := client.GetWriteContext(ctx, &eventstore.GetWriteContextRequest{
Boundary: "accounts",
WriteId: write.WriteId,
})
if err != nil {
return err
}
for _, observation := range writeContext.Consistency {
// observation.Query is the complete query; observation.Position is its observation.
_ = observation
}
const writeContext = await client.getWriteContext({
boundary: 'accounts',
writeId: write.writeId,
});
// writeContext.consistency contains complete query/position observations.
Node.js position components are number | string: safely representable integers
remain numbers, and larger int64 values remain decimal strings. Pass positions
returned by reads directly into cursors and consistency observations. Do not
convert large position strings with Number(), which can round them. Write IDs
are always strings. Application data still uses ordinary JavaScript JSON
parsing; encode application integers beyond JavaScript's safe range as strings
when exact client-side representation is required.
var request = Eventstore.GetWriteContextRequest.newBuilder()
.setBoundary("accounts")
.setWriteId(write.getWriteId())
.build();
var writeContext = client.getWriteContext(request);
// The asynchronous form returns CompletableFuture<Eventstore.WriteContext>.
var pendingContext = client.getWriteContextAsync(request);
An unconditional write has an empty observation list. An unknown write context
returns NOT_FOUND; clients preserve the underlying transport error. Recorded
observations describe the original write and are not fresh reads for a new
command. Read the command's current context before another checked write.
The clients send application data unchanged by storage-envelope conventions:
use the ordinary event-type property on save, use __eventType in criteria and
index definitions, and do not add reserved top-level __* fields to application
data. Returned data has already been stripped by the backend. See the
upgrade guide before upgrading an
existing deployment. The deprecated save methods remain available.
Authenticating from a client​
Every call needs credentials. The typed clients take a username and password at construction and reuse the session token the server returns on later calls, so you only supply Basic credentials once. After changing that user's password, construct a new client with the new password because the server revokes the cached session and the existing client retains its original Basic credentials.
With grpcurl, send HTTP Basic in the authorization metadata header:
grpcurl -H 'Authorization: Basic YWRtaW46Y2hhbmdlaXQ=' localhost:5005 orisun.EventStore/Ping
Each authenticated response sets an x-auth-token header. A long-lived client can capture that token once and send it as x-auth-token on subsequent calls instead of re-sending Basic credentials; Orisun validates the token first and falls back to Basic. Read the full model in Security & Authorization.
Proto Files​
The service definitions live in the main repository:
Generated Go bindings are kept in orisun/.
Generate stubs for another language​
If no official client exists for your language, generate stubs directly from the protobuf files with protoc. For example, for Python:
python -m grpc_tools.protoc \
-I proto \
--python_out=. --grpc_python_out=. \
proto/eventstore.proto proto/admin.proto
Swap the *_out plugins for your target language. With gRPC reflection enabled (the default), you can also explore the API live:
grpcurl -H "$AUTH" localhost:5005 list
grpcurl -H "$AUTH" localhost:5005 describe orisun.EventStore
Compatibility​
Client libraries are generated from the public protobuf definitions. When upgrading Orisun, regenerate or update clients if the protobuf files changed.