├── docs ├── .node-version ├── .eslintignore ├── .vuepress │ ├── public │ │ ├── cloud.png │ │ ├── video.png │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── logo-white.png │ │ ├── fonts │ │ │ ├── Solina-Bold.woff │ │ │ ├── Solina-Bold.woff2 │ │ │ ├── Solina-Light.woff │ │ │ ├── Solina-Light.woff2 │ │ │ ├── Solina-Medium.woff │ │ │ ├── Solina-Medium.woff2 │ │ │ ├── Solina-Regular.woff │ │ │ └── Solina-Regular.woff2 │ │ └── robots.txt │ ├── client.ts │ ├── lib │ │ ├── externalPlaceholders.ts │ │ ├── log.ts │ │ ├── version.ts │ │ └── types.ts │ ├── markdown │ │ ├── xode │ │ │ ├── types.ts │ │ │ ├── normalizeWhitespace.ts │ │ │ └── importCodePlugin.ts │ │ ├── types.ts │ │ ├── resolver.ts │ │ └── replaceLink │ │ │ └── index.ts │ ├── configs │ │ └── theme.ts │ └── config.ts ├── .eslintrc.cjs ├── tsconfig.json ├── package.json └── api │ ├── delete-stream.md │ └── authentication.md ├── src ├── test │ ├── resources │ │ ├── junit-platform.properties │ │ ├── simplelogger.properties │ │ ├── state-with-unknown-keynames.js │ │ ├── count-events-projection.js │ │ ├── count-events-partitioned-projection.js │ │ └── all-versions-filtered-stream194-e0-e30.json │ └── java │ │ └── io │ │ └── kurrent │ │ └── dbclient │ │ ├── Action.java │ │ ├── MiscTests.java │ │ ├── Foo.java │ │ ├── samples │ │ ├── TestEvent.java │ │ ├── authentication │ │ │ └── UserCertificate.java │ │ └── quick_start │ │ │ └── QuickStart.java │ │ ├── misc │ │ ├── EventDataTests.java │ │ ├── ServerVersionTests.java │ │ ├── OfflineMetadataTests.java │ │ └── ParseInvalidConnectionStringTests.java │ │ ├── BazEvent.java │ │ ├── PluginsTests.java │ │ ├── databases │ │ └── ExternallyCreatedCluster.java │ │ ├── persistentsubscriptions │ │ ├── CreatePersistentSubscriptionTests.java │ │ ├── DeletePersistentSubscriptionToStreamTests.java │ │ └── UpdatePersistentSubscriptionToStreamTests.java │ │ ├── StreamsTests.java │ │ ├── telemetry │ │ └── SpanProcessorSpy.java │ │ ├── PersistentSubscriptionsTests.java │ │ ├── streams │ │ ├── InterceptorTests.java │ │ ├── ClientLifecycleTests.java │ │ ├── DeleteTests.java │ │ └── ReadStreamTests.java │ │ ├── plugins │ │ └── ClientCertificateAuthenticationTests.java │ │ ├── Exceptions.java │ │ ├── Database.java │ │ └── DatabaseFactory.java └── main │ ├── java │ └── io │ │ └── kurrent │ │ └── dbclient │ │ ├── Msg.java │ │ ├── OperationKind.java │ │ ├── WorkItem.java │ │ ├── Acl.java │ │ ├── Discovery.java │ │ ├── ContentType.java │ │ ├── Consts.java │ │ ├── SubscriptionTracingCallback.java │ │ ├── ThrowingFunction.java │ │ ├── resolution │ │ ├── NodeResolution.java │ │ ├── DeferredNodeResolution.java │ │ ├── FixedSeedsNodeResolution.java │ │ └── DeprecatedNodeResolution.java │ │ ├── ThrowingBiFunction.java │ │ ├── SystemStreams.java │ │ ├── NoClusterNodeFoundException.java │ │ ├── ConnectionShutdownException.java │ │ ├── EventFilter.java │ │ ├── Direction.java │ │ ├── UnsupportedFeatureException.java │ │ ├── Tuple.java │ │ ├── ResourceNotFoundException.java │ │ ├── SystemMetadataKeys.java │ │ ├── Endpoint.java │ │ ├── StreamNotFoundException.java │ │ ├── ListProjectionsResult.java │ │ ├── ResetProjectionOptions.java │ │ ├── DeleteStreamOptions.java │ │ ├── ListProjectionsOptions.java │ │ ├── EnableProjectionOptions.java │ │ ├── DisableProjectionOptions.java │ │ ├── ConnectionStringParsingException.java │ │ ├── AppendToStreamOptions.java │ │ ├── AbortProjectionOptions.java │ │ ├── GetPersistentSubscriptionInfoOptions.java │ │ ├── GetProjectionStatusOptions.java │ │ ├── ServerInfo.java │ │ ├── ClientFeatureFlags.java │ │ ├── AppendResponse.java │ │ ├── Checkpointer.java │ │ ├── GetProjectionStatisticsOptions.java │ │ ├── ListPersistentSubscriptionsOptions.java │ │ ├── RestartProjectionSubsystemOptions.java │ │ ├── StreamDeletedException.java │ │ ├── StreamTombstonedException.java │ │ ├── DeletePersistentSubscriptionOptions.java │ │ ├── SubscribeToStreamOptions.java │ │ ├── MultiStreamAppendResponse.java │ │ ├── SingleNodeDiscovery.java │ │ ├── Shutdown.java │ │ ├── RestartPersistentSubscriptionSubsystemOptions.java │ │ ├── NodePreference.java │ │ ├── ClientTelemetryConstants.java │ │ ├── NackAction.java │ │ ├── ReadSubscriber.java │ │ ├── DeletePersistentSubscriptionToAll.java │ │ ├── NotLeaderException.java │ │ ├── FeatureFlags.java │ │ ├── AppendStreamRequest.java │ │ ├── UpdateProjectionOptions.java │ │ ├── Acls.java │ │ ├── StreamConsumer.java │ │ ├── RunWorkItem.java │ │ ├── ContentTypeMapper.java │ │ ├── UserCredentials.java │ │ ├── ReplayParkedMessagesOptions.java │ │ ├── SubscribePersistentSubscriptionToAll.java │ │ ├── GetProjectionResultOptions.java │ │ ├── GetProjectionStateOptions.java │ │ ├── SubscribeToAllOptions.java │ │ ├── TransactionMaxSizeExceededException.java │ │ ├── UserStreamAcl.java │ │ ├── SystemStreamAcl.java │ │ ├── SubscribePersistentSubscriptionOptions.java │ │ ├── CreateChannel.java │ │ ├── PrefixFilterExpression.java │ │ ├── SubscribeToStream.java │ │ ├── Subscription.java │ │ ├── DeletePersistentSubscriptionToStream.java │ │ ├── OptionsWithStreamStateBase.java │ │ ├── DeleteResult.java │ │ ├── PersistentSubscriptionListener.java │ │ ├── RestartProjectionSubsystem.java │ │ ├── RegularFilterExpression.java │ │ ├── OptionsWithBackPressure.java │ │ ├── SubscribePersistentSubscriptionToStream.java │ │ ├── OptionsWithResolveLinkTosBase.java │ │ ├── CreateProjectionOptions.java │ │ ├── RevisionOrPosition.java │ │ ├── ClientCertificate.java │ │ ├── ReadStream.java │ │ ├── WrongExpectedVersionException.java │ │ ├── OptionsWithPositionAndResolveLinkTosBase.java │ │ ├── RecordSizeExceededException.java │ │ ├── ConnectionMetadata.java │ │ ├── PersistentSubscriptionToAllInfo.java │ │ ├── PersistentSubscriptionToStreamInfo.java │ │ ├── OptionsWithStartRevisionAndResolveLinkTosBase.java │ │ ├── ClientTelemetryAttributes.java │ │ ├── WriteResult.java │ │ ├── PersistentSubscriptionInfo.java │ │ ├── ResetProjection.java │ │ ├── EnableProjection.java │ │ ├── CreatePersistentSubscriptionToStreamOptions.java │ │ ├── ReadAllOptions.java │ │ ├── AbortProjection.java │ │ ├── ReadStreamOptions.java │ │ ├── SubscriptionListener.java │ │ ├── DisableProjection.java │ │ ├── UpdatePersistentSubscriptionToAll.java │ │ ├── GetProjectionStatus.java │ │ ├── AbstractDeletePersistentSubscription.java │ │ ├── GetProjectionStatistics.java │ │ ├── DeleteProjection.java │ │ ├── ReadStreamConsumer.java │ │ ├── ReadResult.java │ │ ├── AbstractRead.java │ │ ├── SubscribeToAll.java │ │ ├── CreatePersistentSubscriptionToAll.java │ │ ├── UpdatePersistentSubscriptionToStreamOptions.java │ │ ├── RestartPersistentSubscriptionSubsystem.java │ │ ├── ReadAll.java │ │ ├── StreamFilter.java │ │ ├── UpdateProjection.java │ │ ├── EventTypeFilter.java │ │ └── PersistentSubscriptionToStreamStats.java │ └── proto │ └── kurrentdb │ └── protocol │ └── v1 │ ├── serverfeatures.proto │ ├── gossip.proto │ └── shared.proto ├── KurrentLogo-Plum.png ├── KurrentLogo-Black.png ├── KurrentLogo-White.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── CHANGELOG.md ├── vars.env ├── .github ├── workflows │ ├── build-docs.yml │ ├── qa.yml │ ├── cherry-pick-pr-for-label.yml │ ├── ci.yml │ ├── previous-lts.yml │ ├── lts.yml │ ├── build.yml │ ├── publish.yml │ └── load-configuration.yml ├── ISSUE_TEMPLATE │ └── config.yml └── release.yml ├── settings.gradle ├── gradle.properties ├── .git.commit.template ├── configure-user-certs-for-tests.yml └── configure-tls-for-tests.yml /docs/.node-version: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /docs/.eslintignore: -------------------------------------------------------------------------------- 1 | !.vuepress/ 2 | !.*.js 3 | .cache/ 4 | .temp/ 5 | node_modules/ 6 | dist/ -------------------------------------------------------------------------------- /src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.extensions.autodetection.enabled=true -------------------------------------------------------------------------------- /KurrentLogo-Plum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/KurrentLogo-Plum.png -------------------------------------------------------------------------------- /KurrentLogo-Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/KurrentLogo-Black.png -------------------------------------------------------------------------------- /KurrentLogo-White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/KurrentLogo-White.png -------------------------------------------------------------------------------- /docs/.vuepress/public/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/cloud.png -------------------------------------------------------------------------------- /docs/.vuepress/public/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/video.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/favicon.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/.vuepress/public/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/logo-white.png -------------------------------------------------------------------------------- /src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=error 2 | org.slf4j.simpleLogger.log.io.kurrent.dbclient=debug -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Bold.woff -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Bold.woff2 -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Light.woff -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Msg.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | interface Msg { 4 | void accept(ConnectionService handler); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/OperationKind.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | enum OperationKind { 4 | Regular, 5 | Streaming, 6 | } 7 | -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Light.woff2 -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Medium.woff -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Medium.woff2 -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Regular.woff -------------------------------------------------------------------------------- /docs/.vuepress/public/fonts/Solina-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Java/HEAD/docs/.vuepress/public/fonts/Solina-Regular.woff2 -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/WorkItem.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | interface WorkItem { 4 | void accept(WorkItemArgs args, Exception error); 5 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Acl.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Common access control list (ACL) interface. 5 | */ 6 | public interface Acl {} 7 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/Action.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | @FunctionalInterface 4 | public interface Action { 5 | A run() throws Exception; 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This changelog is no longer maintained. The information has been moved to the [GitHub release notes](https://github.com/kurrent-io/KurrentDB-Client-Java/releases) page. 2 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Discovery.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | public interface Discovery { 6 | CompletableFuture run(ConnectionState state); 7 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ContentType.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class ContentType { 4 | public static final String JSON = "application/json"; 5 | public static final String BYTES = "application/octet-stream"; 6 | } -------------------------------------------------------------------------------- /src/test/resources/state-with-unknown-keynames.js: -------------------------------------------------------------------------------- 1 | fromStream('dataset20M-1800'). 2 | when({ 3 | "$any": function(s, e) { 4 | s[e.streamId] = { 5 | timeArrivedMillis: new Date().getTime() 6 | } 7 | } 8 | }).outputState(); 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Consts.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class Consts { 4 | public static long DEFAULT_KEEP_ALIVE_TIMEOUT_IN_MS = 10000; // 10secs 5 | public static long DEFAULT_KEEP_ALIVE_INTERVAL_IN_MS = 10000; // 10secs 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscriptionTracingCallback.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | @FunctionalInterface 4 | public interface SubscriptionTracingCallback { 5 | void trace(String subscriptionId, RecordedEvent event, Runnable action); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ThrowingFunction.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | @FunctionalInterface 4 | interface ThrowingFunction { 5 | 6 | TResult apply(TInput first) throws TException; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/resolution/NodeResolution.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.resolution; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.List; 5 | 6 | public interface NodeResolution { 7 | List resolve(); 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/MiscTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import org.junit.platform.suite.api.SelectPackages; 4 | import org.junit.platform.suite.api.Suite; 5 | 6 | @Suite 7 | @SelectPackages("io.kurrent.dbclient.misc") 8 | public class MiscTests {} 9 | -------------------------------------------------------------------------------- /docs/.vuepress/client.ts: -------------------------------------------------------------------------------- 1 | import type {Router} from "vue-router"; 2 | 3 | interface ClientConfig { 4 | enhance?: (context: { 5 | app: any; 6 | router: Router; 7 | siteData: any; 8 | }) => void | Promise; 9 | setup?: () => void; 10 | } -------------------------------------------------------------------------------- /docs/.vuepress/lib/externalPlaceholders.ts: -------------------------------------------------------------------------------- 1 | const externalPlaceholders = ["@samples", "@clients"]; 2 | 3 | export function isKnownPlaceholder(placeholder: string): boolean { 4 | const known = externalPlaceholders.find(x => x == placeholder); 5 | return known !== undefined; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ThrowingBiFunction.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | @FunctionalInterface 4 | interface ThrowingBiFunction { 5 | 6 | TResult apply(TFirst first, TSecond second) throws TException; 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/count-events-projection.js: -------------------------------------------------------------------------------- 1 | fromStream('dataset20M-1800'). 2 | when({ 3 | "$init": function() { 4 | return { 5 | count: 0 6 | } 7 | }, 8 | "$any": function(s, e) { 9 | s.count = s.count + 1; 10 | } 11 | }).outputState(); 12 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SystemStreams.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class SystemStreams { 4 | public static final String ALL_STREAM = "$all"; 5 | public static final String STREAMS_STREAM = "$streams"; 6 | public static final String SETTINGS_STREAM = "$settings"; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/NoClusterNodeFoundException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * When no node was found based on the connection string provided. 5 | */ 6 | public class NoClusterNodeFoundException extends RuntimeException { 7 | NoClusterNodeFoundException(){} 8 | } 9 | -------------------------------------------------------------------------------- /vars.env: -------------------------------------------------------------------------------- 1 | EVENTSTORE_CLUSTER_SIZE=3 2 | EVENTSTORE_RUN_PROJECTIONS=All 3 | EVENTSTORE_REPLICATION_PORT=1112 4 | EVENTSTORE_NODE_PORT=2113 5 | EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/kurrentdb/certs/ca 6 | EVENTSTORE_DISCOVER_VIA_DNS=false 7 | EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true 8 | EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS=localhost 9 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Production Site 2 | 3 | on: 4 | push: 5 | branches: [release/**/**] 6 | paths: 7 | - '**.md' 8 | 9 | jobs: 10 | dispatch: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Trigger build 14 | run: curl -X POST -d {} "${{ secrets.CLOUDFLARE_BUILD_HOOK }}" -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ConnectionShutdownException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * When a connection is already closed. 5 | */ 6 | public class ConnectionShutdownException extends RuntimeException { 7 | ConnectionShutdownException() { 8 | super("The connection is closed"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/EventFilter.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Optional; 4 | 5 | interface EventFilter { 6 | PrefixFilterExpression[] getPrefixFilterExpressions(); 7 | 8 | RegularFilterExpression getRegularFilterExpression(); 9 | 10 | Optional getMaxSearchWindow(); 11 | } 12 | -------------------------------------------------------------------------------- /docs/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'vuepress', 4 | overrides: [ 5 | { 6 | files: ['*.ts', '*.vue'], 7 | extends: 'vuepress-typescript', 8 | parserOptions: { 9 | project: ['tsconfig.json'], 10 | }, 11 | }, 12 | ], 13 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Direction.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Specifies the direction of a read operation. 5 | */ 6 | public enum Direction { 7 | /** 8 | * Read in the forward direction. 9 | */ 10 | Forwards, 11 | /** 12 | * Read in the backward direction. 13 | */ 14 | Backwards 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UnsupportedFeatureException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * A request not supported by the targeted KurrentDB node was sent. 5 | */ 6 | public class UnsupportedFeatureException extends RuntimeException { 7 | UnsupportedFeatureException(){ 8 | super("Unsupported feature exception"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/count-events-partitioned-projection.js: -------------------------------------------------------------------------------- 1 | fromStream('dataset20M-1800') 2 | .partitionBy(function(event){ 3 | return event.data.somedata % 2 == 1 ? "odd" : "even"; 4 | }) 5 | .when({ 6 | "$init": function() { 7 | return { 8 | count: 0 9 | } 10 | }, 11 | "$any": function(s, e) { 12 | s.count = s.count + 1; 13 | } 14 | }).outputState(); 15 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig-vuepress/base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2022"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "target": "ES2022", 8 | "declaration": false, 9 | "allowJs": true 10 | }, 11 | "include": ["**/.vuepress/**/*"], 12 | "exclude": ["node_modules", ".cache", ".temp", "dist"] 13 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Disallow: /.algolia/ 4 | Disallow: /.github/ 5 | Disallow: /.vscode/ 6 | Disallow: /import/ 7 | Disallow: /.gitattributes 8 | Disallow: /.gitignore 9 | Disallow: /.node-version 10 | Disallow: /codeql-analysis.yml 11 | Disallow: /package.json 12 | Disallow: /serve.js 13 | Disallow: /yarn.lock 14 | 15 | Sitemap: https://docs.kurrent.io/sitemap.xml 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.6.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'kurrentdb-client' 11 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Tuple.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class Tuple { 4 | private final A _1; 5 | private final B _2; 6 | 7 | public Tuple(A _1, B _2) { 8 | this._1 = _1; 9 | this._2 = _2; 10 | } 11 | 12 | public A get_1() { 13 | return this._1; 14 | } 15 | 16 | public B get_2() { 17 | return this._2; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | image: 7 | description: Docker full image name 8 | required: true 9 | type: string 10 | default: "docker.kurrent.io/eventstore/eventstoredb-ee:lts" 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | uses: ./.github/workflows/tests.yml 16 | with: 17 | image: ${{ inputs.image }} 18 | -------------------------------------------------------------------------------- /docs/.vuepress/markdown/xode/types.ts: -------------------------------------------------------------------------------- 1 | export interface ImportCodeTokenMeta { 2 | importPath: string; 3 | lineStart: number; 4 | lineEnd?: number; 5 | region?: string; 6 | } 7 | 8 | export interface ResolvedImport { 9 | label?: string; 10 | importPath: string; 11 | } 12 | 13 | export interface ExtendedCodeImportPluginOptions { 14 | handleImportPath?: (files: string) => ResolvedImport[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/all-versions-filtered-stream194-e0-e30.json: -------------------------------------------------------------------------------- 1 | [ 2 | 194, 3 | 394, 4 | 594, 5 | 794, 6 | 994, 7 | 1194, 8 | 1394, 9 | 1594, 10 | 1794, 11 | 1994, 12 | 140, 13 | 141, 14 | 142, 15 | 143, 16 | 144, 17 | 145, 18 | 146, 19 | 147, 20 | 148, 21 | 149, 22 | 340, 23 | 341, 24 | 342, 25 | 343, 26 | 344, 27 | 345, 28 | 346, 29 | 347, 30 | 348, 31 | 349 32 | ] 33 | -------------------------------------------------------------------------------- /docs/.vuepress/lib/log.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | error(message: string) { 3 | console.log("\x1b[41m%s\x1b[0m", ' ERROR ', `${message}\n`) 4 | process.exit(0) 5 | }, 6 | info(message: string) { 7 | console.log("\x1b[44m%s\x1b[0m", ' INFO ', `${message}\n`) 8 | }, 9 | success(message: string) { 10 | console.log("\x1b[42m\x1b[30m%s\x1b[0m", ' DONE ', `${message}\n`) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | grpcVersion=1.71.0 3 | protocVersion=4.28.2 4 | protobufVersion=4.28.2 5 | annotationApiVersion=1.3.2 6 | validationApiVersion=2.0.1.Final 7 | reactiveStreamsApiVersion=1.0.4 8 | junitVersion=5.10.0 9 | openTelemetryVersion=1.37.0 10 | openTelemetrySemConvVersion=1.25.0-alpha 11 | 12 | # Test Dependencies 13 | slf4jNopVersion=1.7.29 14 | testcontainersVersion=1.20.6 15 | jacksonVersion=2.18.3 16 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * A remote resource was not found or because its access was denied. Could only happen when a request was performed 5 | * through HTTP. 6 | */ 7 | public class ResourceNotFoundException extends RuntimeException { 8 | ResourceNotFoundException(){ 9 | super("Resource not found exception"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SystemMetadataKeys.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class SystemMetadataKeys { 4 | static final String CONTENT_TYPE = "content-type"; 5 | static final String CREATED = "created"; 6 | static final String IS_JSON = "is-json"; 7 | static final String TYPE = "type"; 8 | static final String SCHEMA_NAME = "$schema.name"; 9 | static final String DATA_FORMAT = "$schema.data-format"; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Endpoint.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | public class Endpoint { 4 | private final String host; 5 | private final int port; 6 | 7 | public Endpoint(String host, int port) { 8 | this.host = host; 9 | this.port = port; 10 | } 11 | 12 | public String getHost() { 13 | return host; 14 | } 15 | 16 | public int getPort() { 17 | return port; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/StreamNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * When a stream is not found. 5 | */ 6 | public class StreamNotFoundException extends RuntimeException { 7 | private final String streamName; 8 | 9 | StreamNotFoundException(String streamName){ 10 | this.streamName = streamName; 11 | } 12 | 13 | public String getStreamName() { 14 | return this.streamName; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ListProjectionsResult.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.List; 4 | 5 | class ListProjectionsResult { 6 | private final List projections; 7 | 8 | public ListProjectionsResult(List projections) { 9 | this.projections = projections; 10 | } 11 | 12 | public List getProjections() { 13 | return this.projections; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ResetProjectionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the reset projection request. 5 | */ 6 | public class ResetProjectionOptions extends OptionsBase { 7 | private ResetProjectionOptions() { 8 | } 9 | 10 | /** 11 | * Options with default values. 12 | */ 13 | public static ResetProjectionOptions get() { 14 | return new ResetProjectionOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/kurrent-io/KurrentDB-Client-Java/discussions/new?category=ideas&labels=triage&title=Feature%20request%3A%20 5 | about: Request a feature to add to the client 6 | - name: Ask a Question 7 | url: https://github.com/kurrent-io/KurrentDB-Client-Java/discussions/new?category=q-a&labels=triage 8 | about: Ask questions and discuss with other community members. -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DeleteStreamOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the delete or tombstone stream request. 5 | */ 6 | public class DeleteStreamOptions extends OptionsWithStreamStateBase { 7 | DeleteStreamOptions() {} 8 | 9 | /** 10 | * Returns options with default values. 11 | */ 12 | public static DeleteStreamOptions get() { 13 | return new DeleteStreamOptions(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ListProjectionsOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the list projections options. 5 | */ 6 | public class ListProjectionsOptions extends OptionsBase { 7 | private ListProjectionsOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static ListProjectionsOptions get() { 14 | return new ListProjectionsOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/cherry-pick-pr-for-label.yml: -------------------------------------------------------------------------------- 1 | name: Cherry pick PR commits for label 2 | on: 3 | pull_request_target: 4 | types: [closed] 5 | jobs: 6 | cherry_pick_pr_for_label: 7 | name: Cherry pick PR commits for label 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Cherry Pick PR for label 12 | uses: kurrent-io/Automations/cherry-pick-pr-for-label@master 13 | with: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/EnableProjectionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the enable projection request. 5 | */ 6 | public class EnableProjectionOptions extends OptionsBase { 7 | private EnableProjectionOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static EnableProjectionOptions get() { 14 | return new EnableProjectionOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DisableProjectionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the disable projection request. 5 | */ 6 | public class DisableProjectionOptions extends OptionsBase { 7 | private DisableProjectionOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static DisableProjectionOptions get() { 14 | return new DisableProjectionOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ConnectionStringParsingException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * When the provided connection string is malformed. 5 | */ 6 | public class ConnectionStringParsingException extends Exception { 7 | ConnectionStringParsingException(String connectionString, int from, int to, String expected) { 8 | super("Unexpected " + connectionString.substring(from, from == to ? from + 1 : to) + " at position " + from + ", expected " + expected); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AppendToStreamOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the append stream request. 5 | */ 6 | public class AppendToStreamOptions extends OptionsWithStreamStateBase { 7 | private AppendToStreamOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static AppendToStreamOptions get() { 14 | return new AppendToStreamOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Added 7 | labels: 8 | - enhancement 9 | - title: Fixed 10 | labels: 11 | - bug 12 | - title: Changed 13 | labels: 14 | - "*" 15 | - title: Deprecated 16 | labels: 17 | - deprecated 18 | - title: Breaking Changes 19 | labels: 20 | - breaking 21 | - title: Documentation 22 | labels: 23 | - documentation -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AbortProjectionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the abort projection request. 5 | */ 6 | public class AbortProjectionOptions extends OptionsBase { 7 | private AbortProjectionOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | * @return options 13 | */ 14 | public static AbortProjectionOptions get() { 15 | return new AbortProjectionOptions(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetPersistentSubscriptionInfoOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the get persistent subscription info request. 5 | */ 6 | public class GetPersistentSubscriptionInfoOptions extends OptionsBase { 7 | /** 8 | * Returns options with default values. 9 | */ 10 | public static GetPersistentSubscriptionInfoOptions get() { 11 | return new GetPersistentSubscriptionInfoOptions(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetProjectionStatusOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the get projection status request. 5 | */ 6 | public class GetProjectionStatusOptions extends OptionsBase { 7 | private GetProjectionStatusOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static GetProjectionStatusOptions get() { 14 | return new GetProjectionStatusOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ServerInfo.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class ServerInfo { 4 | private final ServerVersion version; 5 | private final int features; 6 | 7 | ServerInfo(ServerVersion version, int features) { 8 | this.version = version; 9 | this.features = features; 10 | } 11 | 12 | public boolean supportFeature(int feature) { 13 | return (features & feature) != 0; 14 | } 15 | public ServerVersion getServerVersion() { return version; } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "docs/**" 6 | - "samples/**" 7 | - "**.md" 8 | push: 9 | branches: 10 | - trunk 11 | schedule: 12 | - cron: '0 0 * * 0' # Run every Sunday at midnight UTC 13 | 14 | jobs: 15 | build: 16 | uses: ./.github/workflows/build.yml 17 | 18 | ci: 19 | name: Tests (CI) 20 | uses: ./.github/workflows/tests.yml 21 | with: 22 | runtime: ci 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ClientFeatureFlags.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | public final class ClientFeatureFlags { 4 | /** 5 | * Enables direct DNS name resolution, retrieving all IP addresses associated with a given hostname. This 6 | * functionality was initially implemented to support the now-deprecated TCP API. It is particularly useful in 7 | * scenarios involving clusters, where node discovery is enabled. 8 | */ 9 | public static final String DNS_LOOKUP = "dns-lookup"; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AppendResponse.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | public class AppendResponse { 4 | private final String stream; 5 | private final long streamRevision; 6 | 7 | public AppendResponse(String stream, long streamRevision) { 8 | this.stream = stream; 9 | this.streamRevision = streamRevision; 10 | } 11 | 12 | public String getStream() { 13 | return stream; 14 | } 15 | 16 | public long getStreamRevision() { 17 | return streamRevision; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Checkpointer.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | /** 6 | * Callback type when a checkpoint is reached. 7 | */ 8 | @FunctionalInterface 9 | public interface Checkpointer { 10 | /** 11 | * Called everytime a checkpoint is reached. 12 | * @param subscription Subscription handle. 13 | * @param position Transaction log position. 14 | */ 15 | CompletableFuture onCheckpoint(Subscription subscription, Position position); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetProjectionStatisticsOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the get projection statistics request. 5 | */ 6 | public class GetProjectionStatisticsOptions extends OptionsBase { 7 | private GetProjectionStatisticsOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static GetProjectionStatisticsOptions get() { 14 | return new GetProjectionStatisticsOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ListPersistentSubscriptionsOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the list persistent subscriptions request. 5 | */ 6 | public class ListPersistentSubscriptionsOptions extends OptionsBase { 7 | ListPersistentSubscriptionsOptions(){} 8 | 9 | /** 10 | * Returns options with default values. 11 | */ 12 | public static ListPersistentSubscriptionsOptions get() { 13 | return new ListPersistentSubscriptionsOptions(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/.vuepress/lib/version.ts: -------------------------------------------------------------------------------- 1 | const versionRegex = /v((\d+\.)?(\d+\.)?(\*|\d+))/; 2 | const nightly = "nightly"; 3 | const v = { 4 | isVersion: (v: string) => versionRegex.test(v), 5 | parseVersion: (v: string) => versionRegex.exec(v), 6 | getVersion: (path: string): string | undefined => { 7 | if (path.includes(nightly)) { 8 | return nightly; 9 | } 10 | const ref = path.split("#")[0]; 11 | const split = ref.split("/"); 12 | return split.find(x => v.isVersion(x)); 13 | } 14 | }; 15 | export default v; 16 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RestartProjectionSubsystemOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the restart projection subsystem request. 5 | */ 6 | public class RestartProjectionSubsystemOptions extends OptionsBase { 7 | private RestartProjectionSubsystemOptions() { 8 | } 9 | 10 | /** 11 | * Options with default values. 12 | */ 13 | public static RestartProjectionSubsystemOptions get() { 14 | return new RestartProjectionSubsystemOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/previous-lts.yml: -------------------------------------------------------------------------------- 1 | name: Previous LTS 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "docs/**" 6 | - "samples/**" 7 | - "**.md" 8 | push: 9 | branches: 10 | - trunk 11 | schedule: 12 | - cron: '0 0 * * 0' # Run every Sunday at midnight UTC 13 | 14 | jobs: 15 | build: 16 | uses: ./.github/workflows/build.yml 17 | 18 | previous-lts: 19 | name: Tests (Previous LTS) 20 | uses: ./.github/workflows/tests.yml 21 | with: 22 | runtime: previous-lts 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/StreamDeletedException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * When a read or write operation was performed on a deleted stream. 5 | */ 6 | final public class StreamDeletedException extends RuntimeException { 7 | private final String streamName; 8 | 9 | StreamDeletedException(String streamName) { 10 | super(String.format("Stream '%s' is deleted", streamName)); 11 | 12 | this.streamName = streamName; 13 | } 14 | 15 | public String getStreamName() { 16 | return streamName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/StreamTombstonedException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * When a read or write operation was performed on a deleted stream. 5 | */ 6 | final public class StreamTombstonedException extends RuntimeException { 7 | private final String streamName; 8 | 9 | StreamTombstonedException(String streamName) { 10 | super(String.format("Stream '%s' is deleted", streamName)); 11 | 12 | this.streamName = streamName; 13 | } 14 | 15 | public String getStreamName() { 16 | return streamName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DeletePersistentSubscriptionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the delete persistent subscription request. 5 | */ 6 | public class DeletePersistentSubscriptionOptions extends OptionsBase { 7 | private DeletePersistentSubscriptionOptions() { 8 | } 9 | 10 | /** 11 | * Returns options with default values. 12 | */ 13 | public static DeletePersistentSubscriptionOptions get() { 14 | return new DeletePersistentSubscriptionOptions(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribeToStreamOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the subscribe to stream request. 5 | */ 6 | public class SubscribeToStreamOptions extends OptionsWithStartRevisionAndResolveLinkTosBase { 7 | private SubscribeToStreamOptions() { 8 | super(OperationKind.Streaming); 9 | } 10 | 11 | /** 12 | * Returns options with default values. 13 | */ 14 | public static SubscribeToStreamOptions get() { 15 | return new SubscribeToStreamOptions(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/.vuepress/markdown/xode/normalizeWhitespace.ts: -------------------------------------------------------------------------------- 1 | export const normalizeWhitespace = (input: string): string => { 2 | // empty lines before start of snippet 3 | const trimmed = input.replace(/^([ ]*\n)*/, ''); 4 | 5 | // find the shortest indent at the start of line 6 | const shortest = trimmed.match(/^[ ]*(?=\S)/gm)?.reduce((a, b) => a.length <= b.length ? a : b) ?? ''; 7 | 8 | if (shortest.length) { 9 | // remove the shortest indent from each line (if exists) 10 | return trimmed.replace(new RegExp(`^${shortest}`, 'gm'), ''); 11 | } 12 | 13 | return trimmed; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/MultiStreamAppendResponse.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.List; 4 | 5 | public class MultiStreamAppendResponse { 6 | private final long position; 7 | private final List results; 8 | 9 | public MultiStreamAppendResponse(long position, List results) { 10 | this.position = position; 11 | this.results = results; 12 | } 13 | 14 | public long getPosition() { 15 | return position; 16 | } 17 | 18 | public List getResults() { 19 | return results; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SingleNodeDiscovery.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.concurrent.CompletableFuture; 5 | 6 | class SingleNodeDiscovery implements Discovery { 7 | private final Endpoint endpoint; 8 | 9 | SingleNodeDiscovery(Endpoint endpoint) { 10 | this.endpoint = endpoint; 11 | } 12 | 13 | @Override 14 | public CompletableFuture run(ConnectionState state) { 15 | return CompletableFuture.runAsync(() -> state.connect(new InetSocketAddress(this.endpoint.getHost(), this.endpoint.getPort()))); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Shutdown.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.function.Consumer; 4 | 5 | class Shutdown implements Msg { 6 | final Consumer completed; 7 | 8 | public Shutdown(Consumer completed) { 9 | this.completed = completed; 10 | } 11 | 12 | public void complete() { 13 | completed.accept(null); 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return "Shutdown"; 19 | } 20 | 21 | @Override 22 | public void accept(ConnectionService handler) { 23 | handler.shutdown(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RestartPersistentSubscriptionSubsystemOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the restart persistent subscription subsystem request. 5 | */ 6 | public class RestartPersistentSubscriptionSubsystemOptions extends OptionsBase { 7 | RestartPersistentSubscriptionSubsystemOptions(){} 8 | 9 | /** 10 | * Options with the default values. 11 | */ 12 | public static RestartPersistentSubscriptionSubsystemOptions get() { 13 | return new RestartPersistentSubscriptionSubsystemOptions(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/proto/kurrentdb/protocol/v1/serverfeatures.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client.server_features; 3 | option java_package = "io.kurrent.dbclient.proto.serverfeatures"; 4 | import "kurrentdb/protocol/v1/shared.proto"; 5 | 6 | service ServerFeatures { 7 | rpc GetSupportedMethods (event_store.client.Empty) returns (SupportedMethods); 8 | } 9 | 10 | message SupportedMethods { 11 | repeated SupportedMethod methods = 1; 12 | string event_store_server_version = 2; 13 | } 14 | 15 | message SupportedMethod { 16 | string method_name = 1; 17 | string service_name = 2; 18 | repeated string features = 3; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/NodePreference.java: -------------------------------------------------------------------------------- 1 | 2 | package io.kurrent.dbclient; 3 | 4 | /** 5 | * Indicates which order of preferred nodes for connecting to. 6 | */ 7 | public enum NodePreference { 8 | /** 9 | * When attempting connection, prefers leader nodes. 10 | */ 11 | LEADER, 12 | 13 | /** 14 | * When attempting connection, prefers follower nodes. 15 | */ 16 | FOLLOWER, 17 | 18 | /** 19 | * When attempting connection, prefers read-replica nodes. 20 | */ 21 | READ_ONLY_REPLICA, 22 | 23 | /** 24 | * When attempting connection, has no node preference. 25 | */ 26 | RANDOM 27 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ClientTelemetryConstants.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | public class ClientTelemetryConstants { 4 | public static final String INSTRUMENTATION_NAME = "kurrentdb"; 5 | 6 | public static class Metadata { 7 | public static final String TRACE_ID = "$traceId"; 8 | public static final String SPAN_ID = "$spanId"; 9 | } 10 | 11 | public static class Operations { 12 | public static final String APPEND = "streams.append"; 13 | public static final String MULTI_APPEND = "streams.multi-append"; 14 | public static final String SUBSCRIBE = "streams.subscribe"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/NackAction.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Gathers every possible Nak actions. 5 | */ 6 | public enum NackAction { 7 | /** 8 | * Client does not know what action to take, let the server decide. 9 | */ 10 | Unknown, 11 | 12 | /** 13 | * Park message do not resend. Put on poison queue. 14 | */ 15 | Park, 16 | 17 | /** 18 | * Explicit retry the message. 19 | */ 20 | Retry, 21 | 22 | /** 23 | * Skip this message do not resend do not put in poison queue. 24 | */ 25 | Skip, 26 | 27 | /** 28 | * Stop the subscription. 29 | */ 30 | Stop, 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/resolution/DeferredNodeResolution.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.resolution; 2 | 3 | import io.kurrent.dbclient.Endpoint; 4 | 5 | import java.net.InetSocketAddress; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | public class DeferredNodeResolution implements NodeResolution { 10 | private final Endpoint address; 11 | 12 | public DeferredNodeResolution(Endpoint address) { 13 | this.address = address; 14 | } 15 | 16 | @Override 17 | public List resolve() { 18 | return Collections.singletonList(new InetSocketAddress(address.getHost(), address.getPort())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/Foo.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Objects; 4 | 5 | public class Foo { 6 | @Override 7 | public boolean equals(Object o) { 8 | if (this == o) return true; 9 | if (o == null || getClass() != o.getClass()) return false; 10 | Foo foo1 = (Foo) o; 11 | return foo == foo1.foo; 12 | } 13 | 14 | @Override 15 | public int hashCode() { 16 | return Objects.hash(foo); 17 | } 18 | 19 | private boolean foo; 20 | 21 | public boolean isFoo() { 22 | return foo; 23 | } 24 | 25 | public void setFoo(boolean foo) { 26 | this.foo = foo; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /.git.commit.template: -------------------------------------------------------------------------------- 1 | Describe change in active voice, ideally in under 60 chars 2 | 3 | Provide a more detailed summary including the motivations for the change, any 4 | downstream impacts, and limitations of the approaches taken. If other 5 | competing approaches were considered, include a summary of those and why they 6 | were not used instead. The body should describe _why_ the changes were made 7 | rather than the detail of what changes were made, which is easily identifiable 8 | from the diff. 9 | 10 | Wrap the body at 72 characters. 11 | 12 | For more information on writing Git commit messages which are likely to be 13 | merged without modification, see see https://chris.beams.io/posts/git-commit/ 14 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import org.reactivestreams.Subscriber; 4 | import org.reactivestreams.Subscription; 5 | 6 | abstract class ReadSubscriber implements Subscriber { 7 | 8 | private Subscription subscription; 9 | 10 | @Override 11 | public final void onSubscribe(Subscription s) { 12 | this.subscription = s; 13 | } 14 | 15 | public final void request(long n) { 16 | this.subscription.request(n); 17 | } 18 | 19 | @Override 20 | public final void onNext(ReadMessage resolvedEvent) { 21 | onEvent(resolvedEvent); 22 | } 23 | 24 | public abstract void onEvent(ReadMessage resolvedEvent); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DeletePersistentSubscriptionToAll.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | 6 | class DeletePersistentSubscriptionToAll extends AbstractDeletePersistentSubscription { 7 | public DeletePersistentSubscriptionToAll(GrpcClient client, String group, 8 | DeletePersistentSubscriptionOptions options) { 9 | super(client, group, options); 10 | } 11 | 12 | @Override 13 | protected Persistent.DeleteReq.Options.Builder createOptions() { 14 | return Persistent.DeleteReq.Options.newBuilder() 15 | .setAll(Shared.Empty.newBuilder()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/NotLeaderException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | /** 6 | * When a request needing a leader node was executed on a follower node. 7 | * In this case the connection will reconnect automatically to the leader node. However, the request causing that 8 | * exception needs to be retried if the user really wants it to be carried out. 9 | */ 10 | public class NotLeaderException extends RuntimeException { 11 | private final InetSocketAddress leaderEndpoint; 12 | 13 | NotLeaderException(String host, int port) { 14 | leaderEndpoint = new InetSocketAddress(host, port); 15 | } 16 | 17 | public InetSocketAddress getLeaderEndpoint() { 18 | return leaderEndpoint; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/samples/TestEvent.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.samples; 2 | 3 | public class TestEvent { 4 | private String id; 5 | private String importantData; 6 | 7 | public TestEvent(){ 8 | } 9 | 10 | public TestEvent(String id, String importantData){ 11 | this.id = id; 12 | this.importantData = importantData; 13 | } 14 | 15 | public String getId() { 16 | return id; 17 | } 18 | 19 | public void setId(String id){ 20 | this.id = id; 21 | } 22 | 23 | public String getImportantData() { 24 | return importantData; 25 | } 26 | 27 | public void setImportantData(String importantData){ 28 | this.importantData = importantData; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/.vuepress/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type {NavItemOptions, SidebarLinkOptions} from "vuepress-theme-hope"; 2 | 3 | export interface EsSidebarGroupOptions extends NavItemOptions { 4 | group?: string; 5 | collapsible?: boolean; 6 | title?: string; 7 | version?: string; 8 | prefix?: string; 9 | link?: string; 10 | children: EsSidebarItemOptions[] | string[]; 11 | } 12 | 13 | export type EsSidebarItemOptions = SidebarLinkOptions | EsSidebarGroupOptions | string; 14 | export type EsSidebarArrayOptions = EsSidebarItemOptions[]; 15 | export type EsSidebarObjectOptions = Record; 16 | export type EsSidebarOptions = EsSidebarArrayOptions | EsSidebarObjectOptions; 17 | 18 | export type ImportedSidebarArrayOptions = EsSidebarGroupOptions[]; -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/samples/authentication/UserCertificate.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.samples.authentication; 2 | 3 | import io.kurrent.dbclient.KurrentDBClient; 4 | import io.kurrent.dbclient.KurrentDBClientSettings; 5 | import io.kurrent.dbclient.KurrentDBConnectionString; 6 | 7 | public class UserCertificate { 8 | private static void tracing() { 9 | // region client-with-user-certificates 10 | KurrentDBClientSettings settings = KurrentDBConnectionString 11 | .parseOrThrow("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}"); 12 | KurrentDBClient client = KurrentDBClient.create(settings); 13 | // endregion client-with-user-certificates 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /configure-user-certs-for-tests.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | volumes-provisioner: 5 | image: "hasnat/volumes-provisioner" 6 | environment: 7 | PROVISION_DIRECTORIES: "1000:1000:0755:/tmp/certs" 8 | volumes: 9 | - "./certs:/tmp/certs" 10 | network_mode: "none" 11 | 12 | setup: 13 | image: docker.eventstore.com/eventstore-utils/es-gencert-cli:latest 14 | entrypoint: bash 15 | user: "1000:1000" 16 | command: > 17 | -c "mkdir -p ./certs && cd /certs 18 | && es-gencert-cli create-user -username admin 19 | && es-gencert-cli create-user -username invalid 20 | && find . -type f -print0 | xargs -0 chmod 666" 21 | container_name: setup 22 | volumes: 23 | - ./certs:/certs 24 | depends_on: 25 | - volumes-provisioner 26 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/FeatureFlags.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | class FeatureFlags { 3 | public final static int NOTHING = 0; 4 | public final static int BATCH_APPEND = 1; 5 | public final static int PERSISTENT_SUBSCRIPTION_LIST = 2; 6 | public final static int PERSISTENT_SUBSCRIPTION_REPLAY = 4; 7 | public final static int PERSISTENT_SUBSCRIPTION_RESTART_SUBSYSTEM = 8; 8 | public final static int PERSISTENT_SUBSCRIPTION_GET_INFO = 16; 9 | public final static int PERSISTENT_SUBSCRIPTION_TO_ALL = 32; 10 | public final static int MULTI_STREAM_APPEND = 64; 11 | public final static int PERSISTENT_SUBSCRIPTION_MANAGEMENT = PERSISTENT_SUBSCRIPTION_LIST | PERSISTENT_SUBSCRIPTION_REPLAY | PERSISTENT_SUBSCRIPTION_GET_INFO | PERSISTENT_SUBSCRIPTION_RESTART_SUBSYSTEM; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AppendStreamRequest.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Iterator; 4 | 5 | public class AppendStreamRequest { 6 | private final String streamName; 7 | private final Iterator events; 8 | private final StreamState expectedState; 9 | 10 | public AppendStreamRequest(String streamName, Iterator events, StreamState expectedState) { 11 | this.streamName = streamName; 12 | this.events = events; 13 | this.expectedState = expectedState; 14 | } 15 | 16 | public String getStreamName() { 17 | return streamName; 18 | } 19 | 20 | public Iterator getEvents() { 21 | return events; 22 | } 23 | 24 | public StreamState getExpectedState() { 25 | return expectedState; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UpdateProjectionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the update projection request. 5 | */ 6 | public class UpdateProjectionOptions extends OptionsBase { 7 | private boolean emitEnabled; 8 | 9 | private UpdateProjectionOptions() { 10 | } 11 | 12 | /** 13 | * Returns options with default values. 14 | */ 15 | public static UpdateProjectionOptions get() { 16 | return new UpdateProjectionOptions(); 17 | } 18 | 19 | /** 20 | * Allows the projection to write events. 21 | */ 22 | public UpdateProjectionOptions emitEnabled(boolean value) { 23 | this.emitEnabled = value; 24 | return this; 25 | } 26 | 27 | boolean isEmitEnabled() { 28 | return emitEnabled; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/resolution/FixedSeedsNodeResolution.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.resolution; 2 | 3 | import io.kurrent.dbclient.Endpoint; 4 | 5 | import java.net.InetSocketAddress; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | public class FixedSeedsNodeResolution implements NodeResolution { 11 | private final Endpoint[] seeds; 12 | 13 | public FixedSeedsNodeResolution(Endpoint[] seeds) { 14 | this.seeds = seeds; 15 | } 16 | 17 | @Override 18 | public List resolve() { 19 | List addresses = new ArrayList<>(seeds.length); 20 | 21 | for (Endpoint seed : seeds) 22 | addresses.add(new InetSocketAddress(seed.getHost(), seed.getPort())); 23 | 24 | return addresses; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/lts.yml: -------------------------------------------------------------------------------- 1 | name: LTS 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "docs/**" 6 | - "samples/**" 7 | - "**.md" 8 | push: 9 | branches: 10 | - trunk 11 | schedule: 12 | - cron: '0 0 * * 0' # Run every Sunday at midnight UTC 13 | 14 | jobs: 15 | build: 16 | uses: ./.github/workflows/build.yml 17 | 18 | lts: 19 | name: Tests (LTS) 20 | uses: ./.github/workflows/tests.yml 21 | with: 22 | runtime: lts 23 | secrets: inherit 24 | 25 | # Will be removed in the future 26 | plugins-tests: 27 | name: Plugins Tests 28 | 29 | uses: ./.github/workflows/plugins-tests.yml 30 | with: 31 | registry: docker.eventstore.com/eventstore-ee 32 | image: eventstoredb-commercial 33 | tag: 24.2.0-jammy 34 | secrets: inherit 35 | -------------------------------------------------------------------------------- /configure-tls-for-tests.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | volumes-provisioner: 5 | image: "hasnat/volumes-provisioner" 6 | environment: 7 | PROVISION_DIRECTORIES: "1000:1000:0755:/tmp/certs" 8 | volumes: 9 | - "./certs:/tmp/certs" 10 | network_mode: "none" 11 | 12 | setup: 13 | image: docker.eventstore.com/eventstore-utils/es-gencert-cli:latest 14 | entrypoint: bash 15 | user: "1000:1000" 16 | command: > 17 | -c "mkdir -p ./certs && cd /certs 18 | && es-gencert-cli create-ca 19 | && es-gencert-cli create-node -out ./node --dns-names localhost 20 | && es-gencert-cli create-ca -out ./untrusted-ca 21 | && find . -type f -print0 | xargs -0 chmod 666" 22 | container_name: setup 23 | volumes: 24 | - ./certs:/certs 25 | depends_on: 26 | - volumes-provisioner 27 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Acls.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Access control list (ACL) utility class. 5 | */ 6 | public final class Acls { 7 | 8 | /** 9 | * Returns a default stream ACL. 10 | * @see StreamAcl 11 | * @return acl 12 | */ 13 | public static StreamAcl newStreamAcl() { 14 | return new StreamAcl(); 15 | } 16 | 17 | /** 18 | * Returns a default user ACL. 19 | * @see UserStreamAcl 20 | * @return acl 21 | */ 22 | public static Acl newUserStreamAcl() { 23 | return UserStreamAcl.getInstance(); 24 | } 25 | 26 | /** 27 | * Returns a default system ACL. 28 | * @see SystemStreamAcl 29 | * @return acl 30 | */ 31 | public static Acl newSystemStreamAcl() { 32 | return SystemStreamAcl.getInstance(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/StreamConsumer.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.time.Instant; 4 | 5 | public interface StreamConsumer { 6 | default void onSubscribe(org.reactivestreams.Subscription subscription) {} 7 | void onEvent(ResolvedEvent event); 8 | void onSubscriptionConfirmation(String subscriptionId); 9 | void onCheckpoint(long commit, long prepare); 10 | void onStreamNotFound(String streamName); 11 | void onFirstStreamPosition(long position); 12 | void onLastStreamPosition(long position); 13 | void onLastAllStreamPosition(long commit, long prepare); 14 | void onCaughtUp(Instant timestamp, Long streamRevision, Position position); 15 | void onFellBehind(Instant timestamp, Long streamRevision, Position position); 16 | void onCancelled(Throwable exception); 17 | void onComplete(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RunWorkItem.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.UUID; 4 | 5 | class RunWorkItem implements Msg { 6 | final UUID msgId; 7 | final WorkItem item; 8 | 9 | public RunWorkItem(UUID msgId, WorkItem item) { 10 | this.msgId = msgId; 11 | this.item = item; 12 | } 13 | 14 | public UUID getMsgId() { 15 | return msgId; 16 | } 17 | 18 | public WorkItem getItem() { 19 | return item; 20 | } 21 | 22 | public void reportError(Exception e) { 23 | this.item.accept(null, e); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "RunWorkItem[" + msgId + "]"; 29 | } 30 | 31 | @Override 32 | public void accept(ConnectionService connectionService) { 33 | connectionService.process(this); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ContentTypeMapper.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrentdb.protocol.v2.streams.SchemaFormat; 4 | 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class ContentTypeMapper { 10 | private static final Map CONTENT_TYPE_MAP; 11 | 12 | static { 13 | Map map = new HashMap<>(); 14 | map.put("application/json", SchemaFormat.SCHEMA_FORMAT_JSON); 15 | map.put("application/octet-stream", SchemaFormat.SCHEMA_FORMAT_BYTES); 16 | CONTENT_TYPE_MAP = Collections.unmodifiableMap(map); 17 | } 18 | 19 | public static SchemaFormat toSchemaDataFormat(String contentType) { 20 | return CONTENT_TYPE_MAP.getOrDefault(contentType, SchemaFormat.SCHEMA_FORMAT_UNSPECIFIED); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UserCredentials.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.Base64; 5 | 6 | /** 7 | * Holds a login and a password for authenticated requests. 8 | */ 9 | public final class UserCredentials { 10 | private final String username; 11 | private final String base64Encoded; 12 | 13 | public UserCredentials(String username, String password) { 14 | this.username = username; 15 | 16 | byte[] credentialsBytes = String.format("%s:%s", username, password).getBytes(StandardCharsets.US_ASCII); 17 | base64Encoded = String.format("Basic %s", Base64.getEncoder().encodeToString(credentialsBytes)); 18 | } 19 | 20 | String getUsername() { return username; } 21 | 22 | String basicAuthHeader() { 23 | return base64Encoded; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReplayParkedMessagesOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the replay parked messages request. 5 | */ 6 | public class ReplayParkedMessagesOptions extends OptionsBase { 7 | Long stopAt = null; 8 | 9 | ReplayParkedMessagesOptions(){} 10 | 11 | Long getStopAt() { 12 | return stopAt; 13 | } 14 | 15 | /** 16 | * Replay the parked messages until the event revision within the parked messages stream is reached. 17 | */ 18 | public ReplayParkedMessagesOptions stopAt(long value) { 19 | this.stopAt = value; 20 | return this; 21 | } 22 | 23 | /** 24 | * Options with default values. 25 | */ 26 | public static ReplayParkedMessagesOptions get() { 27 | return new ReplayParkedMessagesOptions(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribePersistentSubscriptionToAll.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | 6 | class SubscribePersistentSubscriptionToAll extends AbstractSubscribePersistentSubscription { 7 | public SubscribePersistentSubscriptionToAll(GrpcClient connection, String group, 8 | SubscribePersistentSubscriptionOptions options, 9 | PersistentSubscriptionListener listener) { 10 | super(connection, group, options, listener); 11 | } 12 | 13 | @Override 14 | protected Persistent.ReadReq.Options.Builder createOptions() { 15 | return defaultReadOptions.clone() 16 | .setAll(Shared.Empty.newBuilder()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetProjectionResultOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the get projection result request. 5 | */ 6 | public class GetProjectionResultOptions extends OptionsBase { 7 | private String partition; 8 | 9 | public GetProjectionResultOptions() { 10 | this.partition = ""; 11 | } 12 | 13 | /** 14 | * Returns options with default values. 15 | */ 16 | public static GetProjectionResultOptions get() { 17 | return new GetProjectionResultOptions(); 18 | } 19 | 20 | String getPartition() { 21 | return this.partition; 22 | } 23 | 24 | /** 25 | * Specifies which partition to retrieve the result from. 26 | */ 27 | public GetProjectionResultOptions partition(String partition) { 28 | this.partition = partition; 29 | return this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetProjectionStateOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the get projection state request. 5 | */ 6 | public class GetProjectionStateOptions extends OptionsBase { 7 | private String partition; 8 | 9 | private GetProjectionStateOptions() { 10 | this.partition = ""; 11 | } 12 | 13 | /** 14 | * Returns options with default values. 15 | */ 16 | public static GetProjectionStateOptions get() { 17 | return new GetProjectionStateOptions(); 18 | } 19 | 20 | /** 21 | * Specifies which partition to retrieve the state from. 22 | */ 23 | public GetProjectionStateOptions partition(String partition) { 24 | this.partition = partition; 25 | return this; 26 | } 27 | 28 | String getPartition() { 29 | return this.partition; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Run Tests 2 | on: 3 | workflow_call: 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | java: [ 8, 11, 17 ] 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check Out Sources 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up JDK ${{ matrix.java }} 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: ${{ matrix.java }} 22 | distribution: 'temurin' 23 | 24 | - name: Setup Gradle 25 | uses: gradle/gradle-build-action@v3 26 | with: 27 | gradle-version: 8.13 28 | 29 | - name: Build 30 | run: ./gradlew compileTest 31 | 32 | # Tests that do not require a database connection. 33 | - name: Misc tests 34 | run: ./gradlew ci --tests MiscTests 35 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/misc/EventDataTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.misc; 2 | 3 | import io.kurrent.dbclient.EventData; 4 | import io.kurrent.dbclient.EventDataBuilder; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.UUID; 9 | 10 | public class EventDataTests { 11 | @Test 12 | public void testBinaryConstructorWithId() throws Throwable { 13 | UUID id = UUID.randomUUID(); 14 | 15 | EventData data = EventDataBuilder.binary(id, "type", new byte[]{}).build(); 16 | 17 | Assertions.assertEquals(id, data.getEventId()); 18 | } 19 | 20 | @Test 21 | public void testJsonConstructorWithId() throws Throwable { 22 | UUID id = UUID.randomUUID(); 23 | 24 | EventData data = EventDataBuilder.json(id, "type", new byte[]{}).build(); 25 | 26 | Assertions.assertEquals(id, data.getEventId()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribeToAllOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of a subscription to $all request. 5 | */ 6 | public class SubscribeToAllOptions extends OptionsWithPositionAndResolveLinkTosBase { 7 | private SubscriptionFilter filter; 8 | 9 | private SubscribeToAllOptions() { 10 | super(OperationKind.Streaming); 11 | } 12 | 13 | /** 14 | * Returns options with default values. 15 | */ 16 | public static SubscribeToAllOptions get() { 17 | return new SubscribeToAllOptions(); 18 | } 19 | 20 | SubscriptionFilter getFilter() { 21 | return filter; 22 | } 23 | 24 | /** 25 | * Applies a server-side filter to determine if an event of the subscription should be yielded. 26 | */ 27 | public SubscribeToAllOptions filter(SubscriptionFilter filter) { 28 | this.filter = filter; 29 | return this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/TransactionMaxSizeExceededException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Thrown when an append transaction exceeds the maximum allowed size. 5 | */ 6 | public class TransactionMaxSizeExceededException extends RuntimeException { 7 | /** 8 | * The size of the transaction in bytes. 9 | */ 10 | private final int size; 11 | 12 | /** 13 | * The maximum allowed size of the append transaction in bytes. 14 | */ 15 | private final int maxSize; 16 | 17 | public TransactionMaxSizeExceededException(int size, int maxSize) { 18 | super(String.format("The total size of the append transaction (%d bytes) exceeds the maximum allowed size of %d bytes by %d bytes", size, maxSize, size - maxSize)); 19 | this.size = size; 20 | this.maxSize = maxSize; 21 | } 22 | 23 | public int getSize() { 24 | return size; 25 | } 26 | 27 | public int getMaxSize() { 28 | return maxSize; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/.vuepress/configs/theme.ts: -------------------------------------------------------------------------------- 1 | import type {ThemeOptions} from "vuepress-theme-hope"; 2 | 3 | export const themeOptions: ThemeOptions = { 4 | logo: "/Kurrent Logo - Plum.svg", 5 | logoDark: "/Kurrent Logo - White.svg", 6 | docsDir: 'docs', 7 | editLink: false, 8 | lastUpdated: true, 9 | toc: true, 10 | repo: "https://github.com/kurrent-io", 11 | repoLabel: "GitHub", 12 | repoDisplay: true, 13 | contributors: false, 14 | pure: false, 15 | darkmode:"toggle", 16 | headerDepth: 3, 17 | pageInfo: false, 18 | markdown: { 19 | figure: true, 20 | imgLazyload: true, 21 | imgMark: true, 22 | imgSize: true, 23 | tabs: true, 24 | codeTabs: true, 25 | component: true, 26 | mermaid: true, 27 | highlighter: { 28 | type: "shiki", 29 | themes: { 30 | light: "one-light", 31 | dark: "one-dark-pro", 32 | } 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UserStreamAcl.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Default user stream access control list (ACL). 5 | */ 6 | class UserStreamAcl implements Acl { 7 | public static final String ACL_NAME = "$userStreamAcl"; 8 | private static final UserStreamAcl SINGLETON = new UserStreamAcl(); 9 | 10 | private UserStreamAcl() {} 11 | 12 | static UserStreamAcl deserialize(String source) { 13 | UserStreamAcl acl = null; 14 | 15 | if (source.equals(ACL_NAME)) 16 | acl = SINGLETON; 17 | 18 | return acl; 19 | } 20 | 21 | static UserStreamAcl getInstance() { 22 | return SINGLETON; 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return ACL_NAME.hashCode(); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object obj) { 32 | return (obj instanceof UserStreamAcl); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return ACL_NAME; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/resolution/DeprecatedNodeResolution.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.resolution; 2 | 3 | import io.kurrent.dbclient.Endpoint; 4 | 5 | import java.net.InetAddress; 6 | import java.net.InetSocketAddress; 7 | import java.net.UnknownHostException; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | public class DeprecatedNodeResolution implements NodeResolution { 13 | private final Endpoint address; 14 | 15 | public DeprecatedNodeResolution(Endpoint address) { 16 | this.address = address; 17 | } 18 | 19 | @Override 20 | public List resolve() { 21 | try { 22 | return Arrays.stream(InetAddress.getAllByName(address.getHost())) 23 | .map(addr -> new InetSocketAddress(addr, address.getPort())) 24 | .collect(Collectors.toList()); 25 | } catch (UnknownHostException e) { 26 | throw new RuntimeException(e); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SystemStreamAcl.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Admin stream access control list (ACL). 5 | */ 6 | class SystemStreamAcl implements Acl { 7 | public static final String ACL_NAME = "$systemStreamAcl"; 8 | private static final SystemStreamAcl SINGLETON = new SystemStreamAcl(); 9 | 10 | private SystemStreamAcl() {} 11 | 12 | static SystemStreamAcl deserialize(String source) { 13 | SystemStreamAcl acl = null; 14 | 15 | if (source.equals(ACL_NAME)) 16 | acl = SINGLETON; 17 | 18 | return acl; 19 | } 20 | 21 | static SystemStreamAcl getInstance() { 22 | return SINGLETON; 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return ACL_NAME.hashCode(); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object obj) { 32 | return (obj instanceof SystemStreamAcl); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return ACL_NAME; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribePersistentSubscriptionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the subscribe persistent subscription request. 5 | */ 6 | public class SubscribePersistentSubscriptionOptions extends OptionsBase { 7 | private int bufferSize; 8 | 9 | private SubscribePersistentSubscriptionOptions() { 10 | super(OperationKind.Streaming); 11 | this.bufferSize = 10; 12 | } 13 | 14 | /** 15 | * Returns options with default values. 16 | */ 17 | public static SubscribePersistentSubscriptionOptions get() { 18 | return new SubscribePersistentSubscriptionOptions(); 19 | } 20 | 21 | int getBufferSize() { 22 | return bufferSize; 23 | } 24 | 25 | /** 26 | * Persistent subscription's buffer size. 27 | */ 28 | public SubscribePersistentSubscriptionOptions bufferSize(int value) { 29 | bufferSize = value; 30 | return this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import { dl } from "@mdit/plugin-dl"; 2 | import viteBundler from "@vuepress/bundler-vite"; 3 | import vueDevTools from 'vite-plugin-vue-devtools' 4 | import { defineUserConfig } from "vuepress"; 5 | import { hopeTheme } from "vuepress-theme-hope"; 6 | import { themeOptions } from "./configs/theme"; 7 | import { linkCheckPlugin } from "./markdown/linkCheck"; 8 | import { replaceLinkPlugin } from "./markdown/replaceLink"; 9 | 10 | 11 | export default defineUserConfig({ 12 | base: "/", 13 | dest: "public", 14 | title: "Docs", 15 | description: "Event-native database", 16 | bundler: viteBundler({ viteOptions: { plugins: [vueDevTools(),], } }), 17 | markdown: { importCode: false }, 18 | extendsMarkdown: md => { 19 | md.use(linkCheckPlugin); 20 | md.use(replaceLinkPlugin, { 21 | replaceLink: (link: string, _) => link 22 | .replace("@api", "/api") 23 | .replace("@server/", "/server/{version}/") 24 | }); 25 | md.use(dl); 26 | }, 27 | theme: hopeTheme(themeOptions, { custom: true }), 28 | }); 29 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/CreateChannel.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.StringJoiner; 5 | import java.util.UUID; 6 | 7 | class CreateChannel implements Msg { 8 | final InetSocketAddress channel; 9 | final UUID previousId; 10 | 11 | public CreateChannel(UUID previousId) { 12 | this.channel = null; 13 | this.previousId = previousId; 14 | } 15 | 16 | public CreateChannel(UUID previousId, InetSocketAddress endpoint) { 17 | this.channel = endpoint; 18 | this.previousId = previousId; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return new StringJoiner(", ", CreateChannel.class.getSimpleName() + "[", "]") 24 | .add("endpoint=" + (channel != null ? channel.toString() : "NOT_SET")) 25 | .toString(); 26 | } 27 | 28 | @Override 29 | public void accept(ConnectionService connectionService) { 30 | connectionService.createChannel(this.previousId, this.channel); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/PrefixFilterExpression.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Objects; 5 | 6 | class PrefixFilterExpression implements Comparable { 7 | @NotNull 8 | private final String value; 9 | public PrefixFilterExpression(@NotNull String value) { 10 | this.value = value; 11 | } 12 | @Override 13 | public String toString() { 14 | return this.value; 15 | } 16 | @Override 17 | public boolean equals(Object o) { 18 | if (this == o) return true; 19 | if (o == null || getClass() != o.getClass()) return false; 20 | PrefixFilterExpression that = (PrefixFilterExpression) o; 21 | return value.equals(that.value); 22 | } 23 | 24 | @Override 25 | public int hashCode() { 26 | return Objects.hash(value); 27 | } 28 | 29 | @Override 30 | public int compareTo(PrefixFilterExpression other) { 31 | return this.value.compareTo(other.value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/proto/kurrentdb/protocol/v1/gossip.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client.gossip; 3 | option java_package = "io.kurrent.dbclient.proto.gossip"; 4 | 5 | import "kurrentdb/protocol/v1/shared.proto"; 6 | 7 | service Gossip { 8 | rpc Read (event_store.client.Empty) returns (ClusterInfo); 9 | } 10 | 11 | message ClusterInfo { 12 | repeated MemberInfo members = 1; 13 | } 14 | 15 | message EndPoint { 16 | string address = 1; 17 | uint32 port = 2; 18 | } 19 | 20 | message MemberInfo { 21 | enum VNodeState { 22 | Initializing = 0; 23 | DiscoverLeader = 1; 24 | Unknown = 2; 25 | PreReplica = 3; 26 | CatchingUp = 4; 27 | Clone = 5; 28 | Follower = 6; 29 | PreLeader = 7; 30 | Leader = 8; 31 | Manager = 9; 32 | ShuttingDown = 10; 33 | Shutdown = 11; 34 | ReadOnlyLeaderless = 12; 35 | PreReadOnlyReplica = 13; 36 | ReadOnlyReplica = 14; 37 | ResigningLeader = 15; 38 | } 39 | event_store.client.UUID instance_id = 1; 40 | int64 time_stamp = 2; 41 | VNodeState state = 3; 42 | bool is_alive = 4; 43 | EndPoint http_end_point = 5; 44 | } -------------------------------------------------------------------------------- /docs/.vuepress/markdown/types.ts: -------------------------------------------------------------------------------- 1 | import type {PageFrontmatter, PageHeader} from "vuepress"; 2 | 3 | export type MarkdownHeader = PageHeader; 4 | 5 | export interface MarkdownLink { 6 | raw: string; 7 | relative: string; 8 | absolute: string; 9 | } 10 | 11 | export interface MarkdownEnv { 12 | base?: string; 13 | filePath?: string | null; 14 | filePathRelative?: string | null; 15 | frontmatter?: PageFrontmatter; 16 | headers?: MarkdownHeader[]; 17 | hoistedTags?: string[]; 18 | importedFiles?: string[]; 19 | links?: MarkdownLink[]; 20 | title?: string; 21 | } 22 | 23 | type Nesting = 1 | 0 | -1; 24 | 25 | export interface MdToken { 26 | type: string; 27 | tag: string; 28 | attrs: Array<[string, string]> | null; 29 | map: [number, number] | null; 30 | nesting: Nesting; 31 | level: number; 32 | attrSet(name: string, value: string): void; 33 | attrGet(name: string): string | null; 34 | } 35 | 36 | export interface MdEnv { 37 | base: string; 38 | filePath: string; 39 | filePathRelative: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/BazEvent.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Objects; 4 | 5 | public class BazEvent { 6 | private String name; 7 | private int age; 8 | 9 | public BazEvent() {} 10 | 11 | public BazEvent(String name, int age) { 12 | this.name = name; 13 | this.age = age; 14 | } 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public int getAge() { 21 | return age; 22 | } 23 | 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | 28 | public void setAge(int age) { 29 | this.age = age; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | BazEvent bazEvent = (BazEvent) o; 37 | return age == bazEvent.age && Objects.equals(name, bazEvent.name); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(name, age); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribeToStream.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.shared.Shared; 4 | import io.kurrent.dbclient.proto.streams.StreamsOuterClass; 5 | 6 | class SubscribeToStream extends AbstractRegularSubscription { 7 | private final SubscribeToStreamOptions options; 8 | private final String streamName; 9 | 10 | public SubscribeToStream(GrpcClient client, String streamName, SubscriptionListener listener, SubscribeToStreamOptions options) { 11 | super(client, options); 12 | 13 | this.streamName = streamName; 14 | this.options = options; 15 | 16 | this.listener = listener; 17 | } 18 | 19 | 20 | @Override 21 | protected StreamsOuterClass.ReadReq.Options.Builder createOptions() { 22 | return defaultSubscribeOptions.clone() 23 | .setResolveLinks(this.options.shouldResolveLinkTos()) 24 | .setNoFilter(Shared.Empty.getDefaultInstance()) 25 | .setStream(GrpcUtils.toStreamOptions(this.streamName, this.options.getStartingRevision())); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/Subscription.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.streams.StreamsOuterClass; 4 | import io.grpc.stub.ClientCallStreamObserver; 5 | 6 | /** 7 | * Subscription handle. 8 | */ 9 | public class Subscription { 10 | private final org.reactivestreams.Subscription internal; 11 | private final String subscriptionId; 12 | private final Checkpointer checkpointer; 13 | 14 | Subscription(org.reactivestreams.Subscription internal, String subscriptionId, Checkpointer checkpointer) { 15 | this.internal = internal; 16 | this.subscriptionId = subscriptionId; 17 | this.checkpointer = checkpointer; 18 | } 19 | 20 | /** 21 | * Returns subscription's id. 22 | */ 23 | public String getSubscriptionId() { 24 | return this.subscriptionId; 25 | } 26 | 27 | /** 28 | * Drops the subscription. 29 | */ 30 | public void stop() { 31 | this.internal.cancel(); 32 | } 33 | 34 | Checkpointer getCheckpointer() { 35 | return this.checkpointer; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DeletePersistentSubscriptionToStream.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | import com.google.protobuf.ByteString; 6 | 7 | class DeletePersistentSubscriptionToStream extends AbstractDeletePersistentSubscription { 8 | private String stream; 9 | 10 | public DeletePersistentSubscriptionToStream(GrpcClient client, String stream, String group, 11 | DeletePersistentSubscriptionOptions options) { 12 | super(client, group, options); 13 | this.stream = stream; 14 | } 15 | 16 | @Override 17 | protected Persistent.DeleteReq.Options.Builder createOptions() { 18 | Shared.StreamIdentifier.Builder streamIdentifier = 19 | Shared.StreamIdentifier.newBuilder() 20 | .setStreamName(ByteString.copyFromUtf8(stream)); 21 | 22 | return Persistent.DeleteReq.Options.newBuilder() 23 | .setStreamIdentifier(streamIdentifier); 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/PluginsTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.plugins.ClientCertificateAuthenticationTests; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * PluginsTests require the enterprise edition of KurrentDB (>= 24.2.0). 11 | */ 12 | public class PluginsTests implements ClientCertificateAuthenticationTests { 13 | static private Database database; 14 | static private Logger logger; 15 | 16 | @BeforeAll 17 | public static void setup() { 18 | database = DatabaseFactory.spawnEnterpriseWithPluginsEnabled("UserCertificates"); 19 | logger = LoggerFactory.getLogger(StreamsTests.class); 20 | } 21 | 22 | @Override 23 | public Database getDatabase() { 24 | return database; 25 | } 26 | 27 | @Override 28 | public Logger getLogger() { 29 | return logger; 30 | } 31 | 32 | @AfterAll 33 | public static void cleanup() { 34 | database.dispose(); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/OptionsWithStreamStateBase.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class OptionsWithStreamStateBase extends OptionsBase { 4 | private StreamState streamState; 5 | 6 | protected OptionsWithStreamStateBase() { 7 | this.streamState = StreamState.any(); 8 | } 9 | 10 | StreamState getStreamState() { 11 | return this.streamState; 12 | } 13 | 14 | /** 15 | * Asks the server to check that the stream receiving is at the expected state. 16 | 17 | * @param state - expected revision. 18 | * @return updated options. 19 | */ 20 | @SuppressWarnings("unchecked") 21 | public T streamState(StreamState state) { 22 | this.streamState = state; 23 | return (T) this; 24 | } 25 | 26 | 27 | /** 28 | * Asks the server to check that the stream receiving is at the given expected revision. 29 | 30 | * @param revision - expected revision. 31 | * @return updated options. 32 | */ 33 | public T streamRevision(long revision) { 34 | return streamState(StreamState.streamRevision(revision)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/databases/ExternallyCreatedCluster.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.databases; 2 | 3 | import io.kurrent.dbclient.*; 4 | 5 | public class ExternallyCreatedCluster implements Database { 6 | final private boolean secure; 7 | final private ClientTracker clientTracker; 8 | 9 | public ExternallyCreatedCluster(boolean secure) { 10 | this.secure = secure; 11 | this.clientTracker = new ClientTracker(); 12 | } 13 | 14 | @Override 15 | public ConnectionSettingsBuilder defaultSettingsBuilder() { 16 | return KurrentDBClientSettings 17 | .builder() 18 | .dnsDiscover(true) 19 | .defaultCredentials("admin", "changeit") 20 | .addHost("localhost", 2_113) 21 | .tls(secure) 22 | .tlsVerifyCert(false) 23 | .maxDiscoverAttempts(50) 24 | .defaultDeadline(60_000); 25 | } 26 | 27 | @Override 28 | public ClientTracker getClientTracker() { 29 | return clientTracker; 30 | } 31 | 32 | @Override 33 | public void cleanup() { 34 | // Nothing do to here. 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/.vuepress/markdown/resolver.ts: -------------------------------------------------------------------------------- 1 | import {logger, path} from "vuepress/utils"; 2 | import version from "../lib/version"; 3 | import {instance} from "../lib/versioning"; 4 | 5 | export const resolveVersionedPath = (importPath: string, filePath: string | null | undefined) => { 6 | let importFilePath = importPath; 7 | let error: string | null = null; 8 | 9 | if (!path.isAbsolute(importPath)) { 10 | // if the importPath is a relative path, we need to resolve it 11 | // according to the markdown filePath 12 | if (!filePath) { 13 | logger.error(`Unable to resolve code path: ${filePath}`); 14 | return { 15 | importFilePath: null, 16 | error: 'Error when resolving path', 17 | }; 18 | } 19 | importFilePath = path.resolve(filePath, '..', importPath); 20 | } 21 | 22 | // if (importFilePath.includes("{version}")) { 23 | // const ver = version.getVersion(filePath!) ?? instance.latestSemver; 24 | // if (ver) { 25 | // importFilePath = importFilePath.replace("{version}", ver); 26 | // } 27 | // } 28 | 29 | return {importFilePath, error}; 30 | } -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/persistentsubscriptions/CreatePersistentSubscriptionTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.persistentsubscriptions; 2 | 3 | import io.kurrent.dbclient.*; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public interface CreatePersistentSubscriptionTests extends ConnectionAware { 7 | @Test 8 | default void testCreatePersistentSub() throws Throwable { 9 | KurrentDBPersistentSubscriptionsClient client = getDefaultPersistentSubscriptionClient(); 10 | 11 | client.createToStream(generateName(), generateName()) 12 | .get(); 13 | 14 | client.createToStream(generateName(), generateName(), CreatePersistentSubscriptionToStreamOptions.get().startFrom(2)) 15 | .get(); 16 | } 17 | 18 | @Test 19 | default void testCreatePersistentSubToAll() throws Throwable { 20 | KurrentDBPersistentSubscriptionsClient client = getDefaultPersistentSubscriptionClient(); 21 | 22 | client.createToAll(generateName()) 23 | .get(); 24 | 25 | client.createToAll(generateName(), CreatePersistentSubscriptionToAllOptions.get().startFrom(1, 2)) 26 | .get(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/StreamsTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | 4 | import io.kurrent.dbclient.streams.*; 5 | import io.kurrent.dbclient.streams.ReadStreamTests; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class StreamsTests implements 12 | AppendTests, 13 | ReadStreamTests, 14 | SubscriptionTests, 15 | DeleteTests, 16 | DeadlineTests, 17 | InterceptorTests, 18 | MetadataTests, 19 | ClientLifecycleTests { 20 | static private Database database; 21 | static private Logger logger; 22 | 23 | @BeforeAll 24 | public static void setup() { 25 | database = DatabaseFactory.spawn(); 26 | logger = LoggerFactory.getLogger(StreamsTests.class); 27 | } 28 | 29 | @Override 30 | public Database getDatabase() { 31 | return database; 32 | } 33 | 34 | @Override 35 | public Logger getLogger() { 36 | return logger; 37 | } 38 | 39 | @AfterAll 40 | public static void cleanup() { 41 | database.dispose(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DeleteResult.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Objects; 5 | 6 | /** 7 | * Object returned on a successful stream deletion request. 8 | */ 9 | public class DeleteResult { 10 | private final Position logPosition; 11 | 12 | DeleteResult(@NotNull Position logPosition) { 13 | this.logPosition = logPosition; 14 | } 15 | 16 | /** 17 | * Returns the transaction log position of the stream deletion. 18 | */ 19 | public Position getPosition() { 20 | return logPosition; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) return true; 26 | if (o == null || getClass() != o.getClass()) return false; 27 | DeleteResult that = (DeleteResult) o; 28 | return logPosition.equals(that.logPosition); 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return Objects.hash(logPosition); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "DeleteResult{" + 39 | "logPosition=" + logPosition + 40 | '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/PersistentSubscriptionListener.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Listener used to handle persistent subscription notifications raised throughout its lifecycle. 5 | */ 6 | public abstract class PersistentSubscriptionListener { 7 | /** 8 | * Called when KurrentDB sends an event to the persistent subscription. 9 | * @param subscription handle to the persistent subscription. 10 | * @param retryCount how many times the event was retried. 11 | * @param event a resolved event. 12 | */ 13 | public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) {} 14 | 15 | /** 16 | * Called when the subscription is cancelled or dropped. 17 | * @param subscription handle to the subscription. 18 | * @param exception an exception. null if the user initiated the cancellation. 19 | */ 20 | public void onCancelled(PersistentSubscription subscription, Throwable exception) {} 21 | 22 | /** 23 | * Called when the subscription is confirmed by the server. 24 | * @param subscription handle to the subscription. 25 | */ 26 | public void onConfirmation(PersistentSubscription subscription) {} 27 | } -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RestartProjectionSubsystem.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | class RestartProjectionSubsystem { 9 | private final GrpcClient client; 10 | private final RestartProjectionSubsystemOptions options; 11 | 12 | public RestartProjectionSubsystem(final GrpcClient client, final RestartProjectionSubsystemOptions options) { 13 | this.client = client; 14 | this.options = options; 15 | } 16 | 17 | public CompletableFuture execute() { 18 | return this.client.run(channel -> { 19 | ProjectionsGrpc.ProjectionsStub client = 20 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 21 | 22 | CompletableFuture result = new CompletableFuture<>(); 23 | 24 | Shared.Empty request = Shared.Empty.newBuilder().build(); 25 | 26 | client.restartSubsystem(request, GrpcUtils.convertSingleResponse(result)); 27 | 28 | return result; 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RegularFilterExpression.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Objects; 5 | import java.util.regex.Pattern; 6 | 7 | class RegularFilterExpression implements Comparable { 8 | @NotNull 9 | private final Pattern value; 10 | 11 | public RegularFilterExpression(@NotNull Pattern value) { 12 | this.value = value; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return this.value.pattern(); 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | RegularFilterExpression that = (RegularFilterExpression) o; 25 | return value.equals(that.value); 26 | } 27 | 28 | @Override 29 | public int hashCode() { 30 | return Objects.hash(value); 31 | } 32 | 33 | @Override 34 | public int compareTo(RegularFilterExpression other) { 35 | String ours = this.value.pattern(); 36 | String theirs = other.value.pattern(); 37 | 38 | return ours.compareTo(theirs); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/telemetry/SpanProcessorSpy.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.telemetry; 2 | 3 | import io.opentelemetry.context.Context; 4 | import io.opentelemetry.sdk.trace.ReadWriteSpan; 5 | import io.opentelemetry.sdk.trace.ReadableSpan; 6 | import io.opentelemetry.sdk.trace.SpanProcessor; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.util.List; 10 | import java.util.function.Consumer; 11 | 12 | public class SpanProcessorSpy implements SpanProcessor { 13 | private final List> spanEndedHooks; 14 | 15 | public SpanProcessorSpy(List> spanEndedHooks) { 16 | this.spanEndedHooks = spanEndedHooks; 17 | } 18 | 19 | @Override 20 | public void onStart(@NotNull Context context, @NotNull ReadWriteSpan readWriteSpan) { 21 | // Do nothing. 22 | } 23 | 24 | @Override 25 | public boolean isStartRequired() { 26 | return false; 27 | } 28 | 29 | @Override 30 | public void onEnd(@NotNull ReadableSpan readableSpan) { 31 | spanEndedHooks.forEach(hook -> hook.accept(readableSpan)); 32 | } 33 | 34 | @Override 35 | public boolean isEndRequired() { 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/OptionsWithBackPressure.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class OptionsWithBackPressure extends OptionsWithResolveLinkTosBase { 4 | private int batchSize; 5 | private float thresholdRatio; 6 | 7 | protected OptionsWithBackPressure(OperationKind kind) { 8 | super(kind); 9 | this.batchSize = 512; 10 | this.thresholdRatio = 0.25f; 11 | } 12 | 13 | protected OptionsWithBackPressure() { 14 | this(OperationKind.Streaming); 15 | } 16 | 17 | int getBatchSize() { 18 | return batchSize; 19 | } 20 | 21 | int computeRequestThreshold() { 22 | return (int)(batchSize * thresholdRatio); 23 | } 24 | 25 | /** 26 | * The maximum number of events to read from the server at the time. 27 | */ 28 | @SuppressWarnings("unchecked") 29 | public T batchSize(int batchSize) { 30 | this.batchSize = batchSize; 31 | return (T)this; 32 | } 33 | 34 | /** 35 | * The ratio of the batch size at which more events should be requested from the server. 36 | */ 37 | @SuppressWarnings("unchecked") 38 | public T thresholdRatio(float ratio) { 39 | this.thresholdRatio = ratio; 40 | return (T)this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribePersistentSubscriptionToStream.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | import com.google.protobuf.ByteString; 6 | 7 | class SubscribePersistentSubscriptionToStream extends AbstractSubscribePersistentSubscription { 8 | private final String stream; 9 | 10 | public SubscribePersistentSubscriptionToStream(GrpcClient connection, String stream, String group, 11 | SubscribePersistentSubscriptionOptions options, 12 | PersistentSubscriptionListener listener) { 13 | super(connection, group, options, listener); 14 | 15 | this.stream = stream; 16 | } 17 | 18 | @Override 19 | protected Persistent.ReadReq.Options.Builder createOptions() { 20 | Shared.StreamIdentifier streamIdentifier = 21 | Shared.StreamIdentifier.newBuilder() 22 | .setStreamName(ByteString.copyFromUtf8(stream)) 23 | .build(); 24 | 25 | return defaultReadOptions.clone() 26 | .setStreamIdentifier(streamIdentifier); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/persistentsubscriptions/DeletePersistentSubscriptionToStreamTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.persistentsubscriptions; 2 | 3 | import io.kurrent.dbclient.ConnectionAware; 4 | import io.kurrent.dbclient.KurrentDBPersistentSubscriptionsClient; 5 | import org.junit.jupiter.api.Test; 6 | 7 | public interface DeletePersistentSubscriptionToStreamTests extends ConnectionAware { 8 | @Test 9 | default void testDeletePersistentSub() throws Throwable { 10 | KurrentDBPersistentSubscriptionsClient client = getDefaultPersistentSubscriptionClient(); 11 | String streamName = generateName(); 12 | String groupName = generateName(); 13 | 14 | client.createToStream(streamName, groupName) 15 | .get(); 16 | 17 | client.deleteToStream(streamName, groupName) 18 | .get(); 19 | } 20 | 21 | @Test 22 | default void testDeletePersistentSubToAll() throws Throwable { 23 | KurrentDBPersistentSubscriptionsClient client = getDefaultPersistentSubscriptionClient(); 24 | String groupName = generateName(); 25 | 26 | client.createToAll(groupName) 27 | .get(); 28 | 29 | client.deleteToAll(groupName) 30 | .get(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/OptionsWithResolveLinkTosBase.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class OptionsWithResolveLinkTosBase extends OptionsBase { 4 | private boolean resolveLinkTos; 5 | 6 | protected OptionsWithResolveLinkTosBase(OperationKind kind) { 7 | super(kind); 8 | this.resolveLinkTos = false; 9 | } 10 | 11 | protected OptionsWithResolveLinkTosBase() { 12 | this(OperationKind.Regular); 13 | } 14 | 15 | boolean shouldResolveLinkTos() { 16 | return this.resolveLinkTos; 17 | } 18 | 19 | /** 20 | * Whether the subscription should resolve linkTo events to their linked events. Default: false. 21 | */ 22 | @SuppressWarnings("unchecked") 23 | public T resolveLinkTos(boolean value) { 24 | this.resolveLinkTos = value; 25 | return (T)this; 26 | } 27 | 28 | /** 29 | * Resolve linkTo events to their linked events. 30 | */ 31 | public T resolveLinkTos() { 32 | return this.resolveLinkTos(true); 33 | } 34 | 35 | /** 36 | * Don't resolve linkTo events to their linked events. 37 | */ 38 | public T notResolveLinkTos() { 39 | return this.resolveLinkTos(false); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/CreateProjectionOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options for create projection request. 5 | */ 6 | public class CreateProjectionOptions extends OptionsBase { 7 | private boolean trackEmittedStreams; 8 | private boolean emitEnabled; 9 | 10 | private CreateProjectionOptions() { 11 | this.trackEmittedStreams = false; 12 | } 13 | 14 | /** 15 | * Returns options with default values. 16 | */ 17 | public static CreateProjectionOptions get() { 18 | return new CreateProjectionOptions(); 19 | } 20 | 21 | boolean isTrackingEmittedStreams() { 22 | return trackEmittedStreams; 23 | } 24 | 25 | boolean isEmitEnabled() { 26 | return emitEnabled; 27 | } 28 | 29 | /** 30 | * If true, the projection tracks all streams it creates. 31 | */ 32 | public CreateProjectionOptions trackEmittedStreams(boolean trackEmittedStreams) { 33 | this.trackEmittedStreams = trackEmittedStreams; 34 | return this; 35 | } 36 | 37 | /** 38 | * If true, allows the projection to emit events. 39 | */ 40 | public CreateProjectionOptions emitEnabled(boolean value) { 41 | this.emitEnabled = value; 42 | return this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RevisionOrPosition.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | * Holds a stream revision number or transaction log position. 7 | */ 8 | public class RevisionOrPosition { 9 | private Optional revision = Optional.empty(); 10 | private Optional position = Optional.empty(); 11 | 12 | RevisionOrPosition() {} 13 | 14 | /** 15 | * Returns a stream revision number. 16 | */ 17 | public Optional getRevision() { 18 | return revision; 19 | } 20 | 21 | /** 22 | * Returns a transaction log position. 23 | */ 24 | public Optional getPosition() { 25 | return position; 26 | } 27 | 28 | void setRevision(long revision) { 29 | this.revision = Optional.of(revision); 30 | } 31 | 32 | void setPosition(Position position) { 33 | this.position = Optional.of(position); 34 | } 35 | 36 | /** 37 | * Checks if the object holds a stream revision number. 38 | */ 39 | public boolean isRevisionPresent() { 40 | return revision.isPresent(); 41 | } 42 | 43 | /** 44 | * Checks if the object holds a transaction log position. 45 | */ 46 | public boolean isPositionPresent(){ 47 | return !isRevisionPresent(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ClientCertificate.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Holds a certificate and key for authenticated requests. 7 | */ 8 | public final class ClientCertificate { 9 | private final String certFile; 10 | private final String keyFile; 11 | 12 | public ClientCertificate(String certFile, String keyFile) { 13 | this.certFile = certFile; 14 | this.keyFile = keyFile; 15 | } 16 | 17 | /** 18 | * Certificate for user authentication. 19 | */ 20 | public String getCertFile() { 21 | return certFile; 22 | } 23 | 24 | /** 25 | * Certificate key for user authentication. 26 | */ 27 | public String getKeyFile() { 28 | return keyFile; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object obj) { 33 | if (this == obj) { 34 | return true; 35 | } 36 | if (obj == null || getClass() != obj.getClass()) { 37 | return false; 38 | } 39 | 40 | ClientCertificate other = (ClientCertificate) obj; 41 | 42 | return Objects.equals(certFile, other.certFile) 43 | && Objects.equals(keyFile, other.keyFile); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(certFile, keyFile); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/PersistentSubscriptionsTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.persistentsubscriptions.*; 4 | import io.kurrent.dbclient.persistentsubscriptions.PersistentSubscriptionToAllWithFilterTests; 5 | import org.junit.jupiter.api.AfterAll; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class PersistentSubscriptionsTests implements 11 | CreatePersistentSubscriptionTests, 12 | PersistentSubscriptionManagementTests, 13 | DeletePersistentSubscriptionToStreamTests, 14 | UpdatePersistentSubscriptionToStreamTests, 15 | SubscribePersistentSubscriptionToStreamTests, 16 | PersistentSubscriptionToAllWithFilterTests { 17 | static private Database database; 18 | static private Logger logger; 19 | 20 | @BeforeAll 21 | public static void setup() { 22 | database = DatabaseFactory.spawn(); 23 | logger = LoggerFactory.getLogger(PersistentSubscriptionsTests.class); 24 | } 25 | 26 | @Override 27 | public Database getDatabase() { 28 | return database; 29 | } 30 | 31 | @Override 32 | public Logger getLogger() { 33 | return logger; 34 | } 35 | 36 | @AfterAll 37 | public static void cleanup() { 38 | database.dispose(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadStream.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.shared.Shared; 4 | import io.kurrent.dbclient.proto.streams.StreamsOuterClass; 5 | 6 | class ReadStream extends AbstractRead { 7 | private final String streamName; 8 | private final ReadStreamOptions options; 9 | 10 | public ReadStream(GrpcClient client, String streamName, ReadStreamOptions options) { 11 | super(client, options); 12 | 13 | this.streamName = streamName; 14 | this.options = options; 15 | } 16 | 17 | @Override 18 | public StreamsOuterClass.ReadReq.Options.Builder createOptions() { 19 | return defaultReadOptions.clone() 20 | .setStream(GrpcUtils.toStreamOptions(this.streamName, this.options.getStartingRevision())) 21 | .setResolveLinks(this.options.shouldResolveLinkTos()) 22 | .setCount(this.options.getMaxCount()) 23 | .setControlOption(StreamsOuterClass.ReadReq.Options.ControlOption.newBuilder().setCompatibility(1)) 24 | .setNoFilter(Shared.Empty.getDefaultInstance()) 25 | .setReadDirection(this.options.getDirection() == Direction.Forwards ? 26 | StreamsOuterClass.ReadReq.Options.ReadDirection.Forwards : 27 | StreamsOuterClass.ReadReq.Options.ReadDirection.Backwards); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/WrongExpectedVersionException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import javax.validation.constraints.NotNull; 4 | 5 | /** 6 | * When append request failed the optimistic concurrency on the server. 7 | */ 8 | public class WrongExpectedVersionException extends RuntimeException { 9 | private final String streamName; 10 | private final StreamState expectedState; 11 | private final StreamState actualState; 12 | 13 | WrongExpectedVersionException( 14 | @NotNull String streamName, 15 | @NotNull StreamState expectedState, 16 | @NotNull StreamState actualState) { 17 | super(String.format("Expected %s but got %s instead", expectedState, actualState)); 18 | this.streamName = streamName; 19 | this.expectedState = expectedState; 20 | this.actualState = actualState; 21 | } 22 | 23 | /** 24 | * Returns on which stream the error occurred. 25 | */ 26 | public String getStreamName() { 27 | return streamName; 28 | } 29 | 30 | /** 31 | * Returns the expected stream state by the request. 32 | */ 33 | public StreamState getExpectedState() { 34 | return expectedState; 35 | } 36 | 37 | /** 38 | * Returns the actual stream state when the check was performed. 39 | */ 40 | public StreamState getActualState() { 41 | return actualState; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/streams/InterceptorTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.streams; 2 | 3 | import io.kurrent.dbclient.ConnectionAware; 4 | import io.kurrent.dbclient.KurrentDBClient; 5 | import io.kurrent.dbclient.ReadStreamOptions; 6 | import io.grpc.*; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | public interface InterceptorTests extends ConnectionAware { 13 | @Test 14 | default void testInterceptorIsCalled() { 15 | AtomicInteger atom = new AtomicInteger(0); 16 | KurrentDBClient client = getDatabase().connectWith(opts -> opts.addInterceptor(new MyInterceptor(atom))); 17 | 18 | try { 19 | client.readStream("foobar", ReadStreamOptions.get()).get(); 20 | } catch (Exception e) { 21 | // We don't care. 22 | } 23 | 24 | Assertions.assertEquals(42, atom.get()); 25 | } 26 | 27 | class MyInterceptor implements ClientInterceptor { 28 | final AtomicInteger atom; 29 | 30 | MyInterceptor(AtomicInteger atom) { 31 | this.atom = atom; 32 | } 33 | @Override 34 | public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { 35 | atom.set(42); 36 | return next.newCall(method, callOptions); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/proto/kurrentdb/protocol/v1/shared.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client; 3 | option java_package = "io.kurrent.dbclient.proto.shared"; 4 | import "google/protobuf/empty.proto"; 5 | 6 | message UUID { 7 | oneof value { 8 | Structured structured = 1; 9 | string string = 2; 10 | } 11 | 12 | message Structured { 13 | int64 most_significant_bits = 1; 14 | int64 least_significant_bits = 2; 15 | } 16 | } 17 | message Empty { 18 | } 19 | 20 | message StreamIdentifier { 21 | reserved 1 to 2; 22 | bytes stream_name = 3; 23 | } 24 | 25 | message AllStreamPosition { 26 | uint64 commit_position = 1; 27 | uint64 prepare_position = 2; 28 | } 29 | 30 | message WrongExpectedVersion { 31 | oneof current_stream_revision_option { 32 | uint64 current_stream_revision = 1; 33 | google.protobuf.Empty current_no_stream = 2; 34 | } 35 | oneof expected_stream_position_option { 36 | uint64 expected_stream_position = 3; 37 | google.protobuf.Empty expected_any = 4; 38 | google.protobuf.Empty expected_stream_exists = 5; 39 | google.protobuf.Empty expected_no_stream = 6; 40 | } 41 | } 42 | 43 | message AccessDenied {} 44 | 45 | message StreamDeleted { 46 | StreamIdentifier stream_identifier = 1; 47 | } 48 | 49 | message Timeout {} 50 | 51 | message Unknown {} 52 | 53 | message InvalidTransaction {} 54 | 55 | message MaximumAppendSizeExceeded { 56 | uint32 maxAppendSize = 1; 57 | } 58 | 59 | message BadRequest { 60 | string message = 1; 61 | } -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/plugins/ClientCertificateAuthenticationTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.plugins; 2 | 3 | import io.kurrent.dbclient.*; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.nio.file.Paths; 8 | 9 | public interface ClientCertificateAuthenticationTests extends ConnectionAware { 10 | @Test 11 | default void testClientCertificateAuthenticationWithValidCertificate() { 12 | Assertions.assertDoesNotThrow(() -> { 13 | KurrentDBClient client = getDatabase() 14 | .createClient(getDatabase() 15 | .defaultSettingsBuilder() 16 | .defaultClientCertificate(clientCertificate("admin"), userKey("admin")) 17 | .defaultCredentials(null) 18 | .buildConnectionSettings()); 19 | 20 | client.readAll().get(); 21 | }); 22 | } 23 | 24 | static String clientCertificate(String user) { 25 | return buildCertPath(user, "crt"); 26 | } 27 | 28 | static String userKey(String user) { 29 | return buildCertPath(user, "key"); 30 | } 31 | 32 | static String buildCertPath(String user, String extension) { 33 | String certsPath = Paths.get(System.getProperty("user.dir"), "certs").toAbsolutePath().toString(); 34 | return String.format("%s/user-%s/user-%s.%s", certsPath, user, user, extension); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/OptionsWithPositionAndResolveLinkTosBase.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class OptionsWithPositionAndResolveLinkTosBase extends OptionsWithBackPressure { 4 | private StreamPosition position; 5 | 6 | protected OptionsWithPositionAndResolveLinkTosBase(OperationKind kind) { 7 | super(kind); 8 | this.position = StreamPosition.start(); 9 | } 10 | 11 | protected OptionsWithPositionAndResolveLinkTosBase() { 12 | this(OperationKind.Regular); 13 | } 14 | 15 | StreamPosition getPosition() { 16 | return position; 17 | } 18 | 19 | /** 20 | * Starts from the beginning of the $all stream. 21 | */ 22 | @SuppressWarnings("unchecked") 23 | public T fromStart() { 24 | this.position = StreamPosition.start(); 25 | return (T)this; 26 | } 27 | 28 | /** 29 | * Starts from the end of the $all stream. 30 | */ 31 | @SuppressWarnings("unchecked") 32 | public T fromEnd() { 33 | this.position = StreamPosition.end(); 34 | return (T)this; 35 | } 36 | 37 | /** 38 | * Starts from the given transaction log position. 39 | * @param position transaction log position. 40 | */ 41 | @SuppressWarnings("unchecked") 42 | public T fromPosition(Position position) { 43 | this.position = StreamPosition.position(position); 44 | return (T)this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RecordSizeExceededException.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Thrown when an append record exceeds the maximum allowed size. 5 | */ 6 | public class RecordSizeExceededException extends RuntimeException { 7 | /** 8 | * The name of the stream where the append was attempted. 9 | */ 10 | private final String stream; 11 | 12 | /** 13 | * The identifier of the offending and oversized record. 14 | */ 15 | private final String recordId; 16 | 17 | /** 18 | * The size of the huge record in bytes. 19 | */ 20 | private final int size; 21 | 22 | /** 23 | * The maximum allowed size of a single record that can be appended in bytes. 24 | */ 25 | private final int maxSize; 26 | 27 | public RecordSizeExceededException(String stream, String recordId, int size, int maxSize) { 28 | super(String.format("The size of record %s (%d bytes) exceeds the maximum allowed size of %d bytes by %d bytes", recordId, size, maxSize, size - maxSize)); 29 | this.stream = stream; 30 | this.recordId = recordId; 31 | this.size = size; 32 | this.maxSize = maxSize; 33 | } 34 | 35 | public String getStream() { 36 | return stream; 37 | } 38 | 39 | public String getRecordId() { 40 | return recordId; 41 | } 42 | 43 | public int getSize() { 44 | return size; 45 | } 46 | 47 | public int getMaxSize() { 48 | return maxSize; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ConnectionMetadata.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.grpc.Metadata; 4 | 5 | import java.util.Map; 6 | 7 | class ConnectionMetadata { 8 | private Metadata metadata; 9 | 10 | public ConnectionMetadata() { 11 | this.metadata = new Metadata(); 12 | } 13 | 14 | public ConnectionMetadata authenticated(UserCredentials credentials) { 15 | this.metadata.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), credentials.basicAuthHeader()); 16 | return this; 17 | } 18 | 19 | public boolean hasUserCredentials() { 20 | return this.metadata.containsKey(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)); 21 | } 22 | 23 | public String getUserCredentials() { 24 | return this.metadata.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)); 25 | } 26 | 27 | public ConnectionMetadata requiresLeader() { 28 | this.metadata.put(Metadata.Key.of("requires-leader", Metadata.ASCII_STRING_MARSHALLER), String.valueOf(true)); 29 | return this; 30 | } 31 | 32 | public ConnectionMetadata headers(Map headers) { 33 | for (Map.Entry entry : headers.entrySet()) 34 | this.metadata.put(Metadata.Key.of(entry.getKey(), Metadata.ASCII_STRING_MARSHALLER), entry.getValue()); 35 | 36 | return this; 37 | } 38 | 39 | public Metadata build() { 40 | return this.metadata; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/PersistentSubscriptionToAllInfo.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Persistent subscription to $all info. 5 | */ 6 | public class PersistentSubscriptionToAllInfo extends PersistentSubscriptionInfo { 7 | private PersistentSubscriptionToAllSettings settings; 8 | private PersistentSubscriptionToAllStats stats; 9 | 10 | PersistentSubscriptionToAllInfo(){} 11 | 12 | /** 13 | * The settings used to create the persistent subscription. 14 | */ 15 | public PersistentSubscriptionToAllSettings getSettings() { 16 | return settings; 17 | } 18 | 19 | void setSettings(PersistentSubscriptionToAllSettings settings) { 20 | this.settings = settings; 21 | } 22 | 23 | /** 24 | * Runtime persistent subscription statistics. 25 | */ 26 | public PersistentSubscriptionToAllStats getStats() { 27 | return stats; 28 | } 29 | 30 | void setStats(PersistentSubscriptionToAllStats stats) { 31 | this.stats = stats; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "PersistentSubscriptionToAllInfo{" + 37 | "settings=" + settings + 38 | ", stats=" + stats + 39 | ", eventSource='" + getEventSource() + '\'' + 40 | ", groupName='" + getGroupName() + '\'' + 41 | ", status='" + getStatus() + '\'' + 42 | ", connections=" + getConnections() + 43 | '}'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/PersistentSubscriptionToStreamInfo.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Persistent subscription to stream info. 5 | */ 6 | public class PersistentSubscriptionToStreamInfo extends PersistentSubscriptionInfo { 7 | private PersistentSubscriptionToStreamSettings settings; 8 | private PersistentSubscriptionToStreamStats stats; 9 | 10 | PersistentSubscriptionToStreamInfo(){} 11 | 12 | /** 13 | * The settings used to create the persistent subscription. 14 | */ 15 | public PersistentSubscriptionToStreamSettings getSettings() { 16 | return settings; 17 | } 18 | 19 | void setSettings(PersistentSubscriptionToStreamSettings settings) { 20 | this.settings = settings; 21 | } 22 | 23 | /** 24 | * Runtime persistent subscription statistics. 25 | */ 26 | public PersistentSubscriptionToStreamStats getStats() { 27 | return stats; 28 | } 29 | 30 | void setStats(PersistentSubscriptionToStreamStats stats) { 31 | this.stats = stats; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "PersistentSubscriptionToStreamInfo{" + 37 | "settings=" + settings + 38 | ", stats=" + stats + 39 | ", eventSource='" + getEventSource() + '\'' + 40 | ", groupName='" + getGroupName() + '\'' + 41 | ", status='" + getStatus() + '\'' + 42 | ", connections=" + getConnections() + 43 | '}'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'release/*' 7 | workflow_dispatch: 8 | inputs: 9 | dry_run: 10 | description: 'Perform a dry run release' 11 | type: boolean 12 | required: false 13 | default: true 14 | 15 | jobs: 16 | publish: 17 | name: Publish 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up JDK 8 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: '8' 26 | distribution: 'temurin' 27 | 28 | - name: Setup Gradle 29 | uses: gradle/gradle-build-action@v3 30 | with: 31 | gradle-version: 8.13 32 | 33 | - name: Release 34 | env: 35 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 36 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 37 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 38 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} 39 | JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }} 40 | JRELEASER_GENERIC_TOKEN: ${{ secrets.GH_PAT }} 41 | run: | 42 | if [ "${{ inputs.dry_run }}" = "true" ]; then 43 | ./gradlew publish jreleaserFullRelease -S --dryrun 44 | else 45 | ./gradlew publish jreleaserFullRelease -S 46 | fi 47 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/OptionsWithStartRevisionAndResolveLinkTosBase.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | class OptionsWithStartRevisionAndResolveLinkTosBase extends OptionsWithBackPressure { 4 | private StreamPosition startRevision; 5 | 6 | protected OptionsWithStartRevisionAndResolveLinkTosBase(OperationKind kind) { 7 | super(kind); 8 | this.startRevision = StreamPosition.start(); 9 | } 10 | 11 | protected OptionsWithStartRevisionAndResolveLinkTosBase() { 12 | this(OperationKind.Regular); 13 | } 14 | 15 | StreamPosition getStartingRevision() { 16 | return this.startRevision; 17 | } 18 | 19 | /** 20 | * Starts from a stream position. 21 | */ 22 | @SuppressWarnings("unchecked") 23 | public T fromRevision(StreamPosition startRevision) { 24 | this.startRevision = startRevision; 25 | return (T)this; 26 | } 27 | 28 | /** 29 | * Starts from the beginning of the stream. 30 | */ 31 | public T fromStart() { 32 | return this.fromRevision(StreamPosition.start()); 33 | } 34 | 35 | /** 36 | * Starts from the end of the stream. 37 | */ 38 | public T fromEnd() { 39 | return this.fromRevision(StreamPosition.end()); 40 | } 41 | 42 | /** 43 | * Starts from the given event revision. 44 | */ 45 | public T fromRevision(long revision) { 46 | return this.fromRevision(StreamPosition.position(revision)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ClientTelemetryAttributes.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.opentelemetry.semconv.ExceptionAttributes; 4 | import io.opentelemetry.semconv.ServerAttributes; 5 | import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; 6 | 7 | public class ClientTelemetryAttributes { 8 | public static class Database { 9 | public static final String USER = DbIncubatingAttributes.DB_USER.getKey(); 10 | public static final String SYSTEM = DbIncubatingAttributes.DB_SYSTEM.getKey(); 11 | public static final String OPERATION = DbIncubatingAttributes.DB_OPERATION.getKey(); 12 | } 13 | 14 | public static class Server { 15 | public static final String ADDRESS = ServerAttributes.SERVER_ADDRESS.getKey(); 16 | public static final String PORT = ServerAttributes.SERVER_PORT.getKey(); 17 | } 18 | 19 | public static class Exceptions { 20 | public static final String TYPE = ExceptionAttributes.EXCEPTION_TYPE.getKey(); 21 | public static final String MESSAGE = ExceptionAttributes.EXCEPTION_MESSAGE.getKey(); 22 | public static final String STACK_TRACE = ExceptionAttributes.EXCEPTION_STACKTRACE.getKey(); 23 | } 24 | 25 | public static class KurrentDB { 26 | public static final String STREAM = "db.kurrentdb.stream"; 27 | public static final String SUBSCRIPTION_ID = "db.kurrentdb.subscription.id"; 28 | public static final String EVENT_ID = "db.kurrentdb.event.id"; 29 | public static final String EVENT_TYPE = "db.kurrentdb.event.type"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/WriteResult.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Returned after writing to a stream. 7 | */ 8 | public class WriteResult { 9 | private final StreamState nextExpectedRevision; 10 | private final Position logPosition; 11 | 12 | WriteResult(StreamState nextExpectedRevision, Position logPosition) { 13 | this.nextExpectedRevision = nextExpectedRevision; 14 | this.logPosition = logPosition; 15 | } 16 | 17 | /** 18 | * Next expected version of the stream. 19 | */ 20 | public StreamState getNextExpectedRevision() { 21 | return nextExpectedRevision; 22 | } 23 | 24 | /** 25 | * Transaction log position of the write. 26 | */ 27 | public Position getLogPosition() { 28 | return logPosition; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) return true; 34 | if (o == null || getClass() != o.getClass()) return false; 35 | WriteResult that = (WriteResult) o; 36 | return nextExpectedRevision == that.nextExpectedRevision && 37 | logPosition.equals(that.logPosition); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(nextExpectedRevision, logPosition); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "WriteResult{" + 48 | "nextExpectedRevision=" + nextExpectedRevision + 49 | ", logPosition=" + logPosition + 50 | '}'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/Exceptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.grpc.Status; 4 | import io.grpc.StatusRuntimeException; 5 | 6 | import java.util.ArrayList; 7 | import java.util.function.Predicate; 8 | 9 | public class Exceptions { 10 | final private ArrayList> exceptions; 11 | 12 | public Exceptions() { 13 | exceptions = new ArrayList<>(); 14 | } 15 | 16 | public Exceptions register(Predicate predicate) { 17 | this.exceptions.add(e -> { 18 | @SuppressWarnings("unchecked") 19 | A target = (A) e; 20 | return target != null && predicate.test(target); 21 | }); 22 | 23 | return this; 24 | } 25 | 26 | public Exceptions register(Class clazz) { 27 | this.exceptions.add(clazz::isInstance); 28 | return this; 29 | } 30 | 31 | public Exceptions registerGoAwayError() { 32 | return this.register(e -> 33 | e.getStatus().getCode() == Status.Code.INTERNAL 34 | ); 35 | } 36 | 37 | public Exceptions registerUnknownError() { 38 | return this.register(e -> 39 | e.getStatus().getCode() == Status.Code.UNKNOWN 40 | ); 41 | } 42 | 43 | public boolean contains(Throwable target) { 44 | for (Predicate predicate : this.exceptions) { 45 | if (predicate.test(target)) { 46 | return true; 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/persistentsubscriptions/UpdatePersistentSubscriptionToStreamTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.persistentsubscriptions; 2 | 3 | import io.kurrent.dbclient.*; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public interface UpdatePersistentSubscriptionToStreamTests extends ConnectionAware { 7 | @Test 8 | default void testUpdatePersistentSub() throws Throwable { 9 | String streamName = generateName(); 10 | String groupName = generateName(); 11 | 12 | KurrentDBPersistentSubscriptionsClient client = getDefaultPersistentSubscriptionClient(); 13 | client.createToStream(streamName, groupName) 14 | .get(); 15 | 16 | UpdatePersistentSubscriptionToStreamOptions updated = UpdatePersistentSubscriptionToStreamOptions.get() 17 | .checkpointAfterInMs(5_000) 18 | .startFrom(2); 19 | 20 | client.updateToStream(streamName, groupName, updated) 21 | .get(); 22 | } 23 | 24 | @Test 25 | default void testUpdatePersistentSubToAll() throws Throwable { 26 | String groupName = generateName(); 27 | KurrentDBPersistentSubscriptionsClient client = getDefaultPersistentSubscriptionClient(); 28 | 29 | client.createToAll(groupName) 30 | .get(); 31 | UpdatePersistentSubscriptionToAllOptions updatedSettings = UpdatePersistentSubscriptionToAllOptions.get() 32 | .checkpointAfterInMs(5_000) 33 | .startFrom(3,4); 34 | 35 | client.updateToAll(groupName, updatedSettings) 36 | .get(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/Database.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.Optional; 7 | import java.util.function.Function; 8 | 9 | public interface Database { 10 | Logger logger = LoggerFactory.getLogger(Database.class); 11 | 12 | ConnectionSettingsBuilder defaultSettingsBuilder(); 13 | 14 | ClientTracker getClientTracker(); 15 | 16 | void cleanup(); 17 | 18 | default KurrentDBClient newClient() { 19 | return connectWith(Function.identity()); 20 | } 21 | 22 | default KurrentDBClient connectWith(Function mod) { 23 | return createClient(mod.apply(defaultSettingsBuilder()).buildConnectionSettings()); 24 | } 25 | 26 | default KurrentDBClient defaultClient() { 27 | return getClientTracker().getDefaultClient(this); 28 | } 29 | 30 | default KurrentDBClient createClient(KurrentDBClientSettings settings) { 31 | return getClientTracker().createClient(settings); 32 | } 33 | 34 | default boolean isTargetingBelowOrEqual21_10() { 35 | try { 36 | Optional result = defaultClient().getServerVersion().get(); 37 | return !result.isPresent() || result.get().isLessOrEqualThan(21, 10); 38 | } catch (Exception e) { 39 | logger.error("Error when retrieving the server version", e); 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | 44 | default void dispose() { 45 | cleanup(); 46 | getClientTracker().dispose(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/PersistentSubscriptionInfo.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Common persistent subscription info type. 7 | */ 8 | public abstract class PersistentSubscriptionInfo { 9 | private String eventSource; 10 | private String groupName; 11 | private String status; 12 | private List connections; 13 | 14 | PersistentSubscriptionInfo(){} 15 | 16 | /** 17 | * The source of events for the subscription. 18 | */ 19 | public String getEventSource() { 20 | return eventSource; 21 | } 22 | 23 | void setEventSource(String eventSource) { 24 | this.eventSource = eventSource; 25 | } 26 | 27 | /** 28 | * The group name given on creation. 29 | */ 30 | public String getGroupName() { 31 | return groupName; 32 | } 33 | 34 | void setGroupName(String groupName) { 35 | this.groupName = groupName; 36 | } 37 | 38 | /** 39 | * The current status of the subscription. 40 | */ 41 | public String getStatus() { 42 | return status; 43 | } 44 | 45 | void setStatus(String status) { 46 | this.status = status; 47 | } 48 | 49 | /** 50 | * Active connections to the subscription. 51 | * @see PersistentSubscriptionConnectionInfo 52 | */ 53 | public List getConnections() { 54 | return connections; 55 | } 56 | 57 | void setConnections(List connections) { 58 | this.connections = connections; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ResetProjection.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | class ResetProjection { 9 | private final GrpcClient client; 10 | private final String projectionName; 11 | private final ResetProjectionOptions options; 12 | 13 | public ResetProjection(final GrpcClient client, final String projectionName, final ResetProjectionOptions options) { 14 | this.client = client; 15 | this.projectionName = projectionName; 16 | this.options = options; 17 | } 18 | 19 | public CompletableFuture execute() { 20 | return this.client.run(channel -> { 21 | Projectionmanagement.ResetReq.Options.Builder optionsBuilder = 22 | Projectionmanagement.ResetReq.Options.newBuilder() 23 | .setName(this.projectionName); 24 | 25 | Projectionmanagement.ResetReq request = Projectionmanagement.ResetReq.newBuilder() 26 | .setOptions(optionsBuilder) 27 | .build(); 28 | 29 | ProjectionsGrpc.ProjectionsStub client = 30 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 31 | 32 | CompletableFuture result = new CompletableFuture<>(); 33 | 34 | client.reset(request, GrpcUtils.convertSingleResponse(result)); 35 | 36 | return result; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/EnableProjection.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | class EnableProjection { 9 | private final GrpcClient client; 10 | private final String projectionName; 11 | private EnableProjectionOptions options; 12 | 13 | public EnableProjection(final GrpcClient client, final String projectionName, final EnableProjectionOptions options) { 14 | this.client = client; 15 | this.projectionName = projectionName; 16 | this.options = options; 17 | } 18 | 19 | public CompletableFuture execute() { 20 | return this.client.run(channel -> { 21 | Projectionmanagement.EnableReq.Options.Builder optionsBuilder = 22 | Projectionmanagement.EnableReq.Options.newBuilder() 23 | .setName(this.projectionName); 24 | 25 | Projectionmanagement.EnableReq request = Projectionmanagement.EnableReq.newBuilder() 26 | .setOptions(optionsBuilder) 27 | .build(); 28 | 29 | ProjectionsGrpc.ProjectionsStub client = 30 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 31 | 32 | CompletableFuture result = new CompletableFuture<>(); 33 | 34 | client.enable(request, GrpcUtils.convertSingleResponse(result)); 35 | 36 | return result; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/.vuepress/markdown/xode/importCodePlugin.ts: -------------------------------------------------------------------------------- 1 | import type {PluginWithOptions} from "markdown-it"; 2 | import type {MarkdownEnv} from "../types"; 3 | import {resolveImportCode} from "./resolveImportCode"; 4 | import {createImportCodeBlockRule} from "./createImportCodeBlockRule"; 5 | import {type ExtendedCodeImportPluginOptions} from "./types"; 6 | import { normalizeWhitespace } from "./normalizeWhitespace"; 7 | 8 | export const importCodePlugin: PluginWithOptions = ( 9 | md, 10 | options = {} 11 | ): void => { 12 | // add import_code block rule 13 | md.block.ruler.before( 14 | 'fence', 15 | 'import_code', 16 | createImportCodeBlockRule(options), 17 | { 18 | alt: ['paragraph', 'reference', 'blockquote', 'list'], 19 | } 20 | ); 21 | 22 | // add import_code renderer rule 23 | md.renderer.rules.import_code = ( 24 | tokens, 25 | idx, 26 | options, 27 | env: MarkdownEnv, 28 | slf 29 | ) => { 30 | const token = tokens[idx]; 31 | 32 | // use imported code as token content 33 | const {importFilePath, importCode} = resolveImportCode(token.meta, env); 34 | token.content = normalizeWhitespace(importCode); 35 | 36 | // extract imported files to env 37 | if (importFilePath) { 38 | const importedFiles = env.importedFiles || (env.importedFiles = []); 39 | importedFiles.push(importFilePath); 40 | } 41 | 42 | // render the import_code token as a fence token 43 | return md.renderer.rules.fence!(tokens, idx, options, env, slf); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/CreatePersistentSubscriptionToStreamOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options for the create persistent subscription to stream request. 5 | */ 6 | public class CreatePersistentSubscriptionToStreamOptions 7 | extends AbstractPersistentSubscriptionSettingsBuilder { 8 | CreatePersistentSubscriptionToStreamOptions() { 9 | super(PersistentSubscriptionSettings.defaultRegular()); 10 | } 11 | 12 | /** 13 | * Returns options with default values. 14 | */ 15 | public static CreatePersistentSubscriptionToStreamOptions get() { 16 | return new CreatePersistentSubscriptionToStreamOptions(); 17 | } 18 | 19 | /** 20 | * Starts the subscription from the beginning of the given stream. 21 | 22 | */ 23 | public CreatePersistentSubscriptionToStreamOptions fromStart() { 24 | getSettings().setStartFrom(StreamPosition.start()); 25 | return this; 26 | } 27 | 28 | /** 29 | * Starts the subscription from the end of the given stream. 30 | 31 | */ 32 | public CreatePersistentSubscriptionToStreamOptions fromEnd() { 33 | getSettings().setStartFrom(StreamPosition.end()); 34 | return this; 35 | } 36 | 37 | /** 38 | * Starts the subscription from the given stream revision. 39 | */ 40 | public CreatePersistentSubscriptionToStreamOptions startFrom(long revision) { 41 | getSettings().setStartFrom(StreamPosition.position(revision)); 42 | return this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadAllOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the read $all stream request. 5 | */ 6 | public class ReadAllOptions extends OptionsWithPositionAndResolveLinkTosBase { 7 | private Direction direction; 8 | private long maxCount; 9 | 10 | private ReadAllOptions() { 11 | this.direction = Direction.Forwards; 12 | this.maxCount = Long.MAX_VALUE; 13 | } 14 | 15 | /** 16 | * Returns options with default values. 17 | */ 18 | public static ReadAllOptions get() { 19 | return new ReadAllOptions(); 20 | } 21 | 22 | Direction getDirection() { 23 | return this.direction; 24 | } 25 | 26 | long getMaxCount() { 27 | return this.maxCount; 28 | } 29 | 30 | /** 31 | * Reads stream in the given direction. 32 | */ 33 | public ReadAllOptions direction(Direction direction) { 34 | this.direction = direction; 35 | return this; 36 | } 37 | 38 | /** 39 | * Reads stream in revision-ascending order. 40 | */ 41 | public ReadAllOptions forwards() { 42 | return direction(Direction.Forwards); 43 | } 44 | 45 | /** 46 | * Reads stream in revision-descending order. 47 | */ 48 | public ReadAllOptions backwards() { 49 | return direction(Direction.Backwards); 50 | } 51 | 52 | /** 53 | * The maximum event count KurrentDB will return. 54 | */ 55 | public ReadAllOptions maxCount(long maxCount) { 56 | this.maxCount = maxCount; 57 | return this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AbortProjection.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | class AbortProjection { 9 | private final GrpcClient client; 10 | private final String projectionName; 11 | private final AbortProjectionOptions options; 12 | 13 | public AbortProjection(final GrpcClient client, final String projectionName, final AbortProjectionOptions options) { 14 | this.client = client; 15 | this.projectionName = projectionName; 16 | this.options = options; 17 | } 18 | 19 | public CompletableFuture execute() { 20 | return this.client.run(channel -> { 21 | Projectionmanagement.DisableReq.Options.Builder optionsBuilder = 22 | Projectionmanagement.DisableReq.Options.newBuilder() 23 | .setName(this.projectionName) 24 | .setWriteCheckpoint(false); 25 | 26 | Projectionmanagement.DisableReq request = Projectionmanagement.DisableReq.newBuilder() 27 | .setOptions(optionsBuilder) 28 | .build(); 29 | 30 | ProjectionsGrpc.ProjectionsStub client = 31 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 32 | 33 | CompletableFuture result = new CompletableFuture<>(); 34 | 35 | client.disable(request, GrpcUtils.convertSingleResponse(result)); 36 | 37 | return result; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadStreamOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the read stream request. 5 | */ 6 | public class ReadStreamOptions extends OptionsWithStartRevisionAndResolveLinkTosBase { 7 | private Direction direction; 8 | private long maxCount; 9 | 10 | private ReadStreamOptions() { 11 | this.direction = Direction.Forwards; 12 | this.maxCount = Long.MAX_VALUE; 13 | } 14 | 15 | /** 16 | * Returns options with default values. 17 | */ 18 | public static ReadStreamOptions get() { 19 | return new ReadStreamOptions(); 20 | } 21 | 22 | Direction getDirection() { 23 | return this.direction; 24 | } 25 | 26 | long getMaxCount() { 27 | return this.maxCount; 28 | } 29 | 30 | /** 31 | * Reads stream in the given direction. 32 | 33 | */ 34 | public ReadStreamOptions direction(Direction direction) { 35 | this.direction = direction; 36 | return this; 37 | } 38 | 39 | /** 40 | * Reads stream in revision-ascending order. 41 | 42 | */ 43 | public ReadStreamOptions forwards() { 44 | return direction(Direction.Forwards); 45 | } 46 | 47 | /** 48 | * Reads stream in revision-descending order. 49 | 50 | */ 51 | public ReadStreamOptions backwards() { 52 | return direction(Direction.Backwards); 53 | } 54 | 55 | /** 56 | * The maximum event count EventStoreDB will return. 57 | */ 58 | public ReadStreamOptions maxCount(long maxCount) { 59 | this.maxCount = maxCount; 60 | return this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscriptionListener.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.time.Instant; 4 | 5 | /** 6 | * Listener used to handle catch-up subscription notifications raised throughout its lifecycle. 7 | */ 8 | public abstract class SubscriptionListener { 9 | /** 10 | * Called when EventStoreDB sends an event to the subscription. 11 | * @param subscription handle to the subscription. 12 | * @param event a resolved event. 13 | */ 14 | public void onEvent(Subscription subscription, ResolvedEvent event) {} 15 | 16 | /** 17 | * Called when the subscription is cancelled or dropped. 18 | * @param subscription handle to the subscription. 19 | * @param exception an exception. null if the user initiated the cancellation. 20 | */ 21 | public void onCancelled(Subscription subscription, Throwable exception) {} 22 | 23 | /** 24 | * Called when the subscription is confirmed by the server. 25 | * @param subscription handle to the subscription. 26 | */ 27 | public void onConfirmation(Subscription subscription) {} 28 | 29 | /** 30 | * Called when the subscription has reached the head of the stream. 31 | * @param subscription handle to the subscription. 32 | */ 33 | public void onCaughtUp(Subscription subscription, Instant timestamp, Long streamRevision, Position position) {} 34 | 35 | /** 36 | * Called when the subscription has fallen behind, meaning it's no longer keeping up with the 37 | * stream's pace. 38 | * @param subscription handle to the subscription. 39 | */ 40 | public void onFellBehind(Subscription subscription, Instant timestamp, Long streamRevision, Position position) {} 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DisableProjection.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | class DisableProjection { 9 | private final GrpcClient client; 10 | private final String projectionName; 11 | private final DisableProjectionOptions options; 12 | 13 | public DisableProjection(final GrpcClient client, final String projectionName, final DisableProjectionOptions options) { 14 | this.client = client; 15 | this.projectionName = projectionName; 16 | this.options = options; 17 | } 18 | 19 | public CompletableFuture execute() { 20 | return this.client.run(channel -> { 21 | Projectionmanagement.DisableReq.Options.Builder optionsBuilder = 22 | Projectionmanagement.DisableReq.Options.newBuilder() 23 | .setName(this.projectionName) 24 | .setWriteCheckpoint(true); 25 | 26 | Projectionmanagement.DisableReq request = Projectionmanagement.DisableReq.newBuilder() 27 | .setOptions(optionsBuilder) 28 | .build(); 29 | 30 | ProjectionsGrpc.ProjectionsStub client = 31 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 32 | 33 | CompletableFuture result = new CompletableFuture<>(); 34 | 35 | client.disable(request, GrpcUtils.convertSingleResponse(result)); 36 | 37 | return result; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/load-configuration.yml: -------------------------------------------------------------------------------- 1 | name: Load KurrentDB Runtime Configuration 2 | on: 3 | workflow_call: 4 | inputs: 5 | runtime: 6 | description: "The runtime's name. Current options are: `ci`, `previous-lts`, `latest`" 7 | type: string 8 | 9 | outputs: 10 | runtime: 11 | description: The runtime's name 12 | value: ${{ inputs.runtime }} 13 | 14 | registry: 15 | description: The Docker registry 16 | value: ${{ jobs.load.outputs.registry }} 17 | 18 | image: 19 | description: The Docker image 20 | value: ${{ jobs.load.outputs.image }} 21 | 22 | tag: 23 | description: The Docker image tag 24 | value: ${{ jobs.load.outputs.tag }} 25 | 26 | full_image_name: 27 | description: The full Docker image name (including registry, image, and tag) 28 | value: ${{ jobs.load.outputs.full_image_name }} 29 | 30 | jobs: 31 | load: 32 | runs-on: ubuntu-latest 33 | outputs: 34 | registry: ${{ steps.set.outputs.registry }} 35 | image: ${{ steps.set.outputs.image }} 36 | tag: ${{ steps.set.outputs.tag }} 37 | full_image_name: ${{ steps.set.outputs.full_image_name }} 38 | 39 | steps: 40 | - name: Set KurrentDB Runtime Configuration Properties 41 | id: set 42 | run: | 43 | echo "registry=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].registry }}" >> $GITHUB_OUTPUT 44 | echo "tag=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].tag }}" >> $GITHUB_OUTPUT 45 | echo "image=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].image }}" >> $GITHUB_OUTPUT 46 | echo "full_image_name=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].fullname }}" >> $GITHUB_OUTPUT 47 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UpdatePersistentSubscriptionToAll.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | 6 | class UpdatePersistentSubscriptionToAll extends AbstractUpdatePersistentSubscription { 7 | private final UpdatePersistentSubscriptionToAllOptions options; 8 | public UpdatePersistentSubscriptionToAll(GrpcClient connection, String group, 9 | UpdatePersistentSubscriptionToAllOptions options) { 10 | super(connection, group, options.getSettings(), options); 11 | 12 | this.options = options; 13 | } 14 | 15 | @Override 16 | protected Persistent.UpdateReq.Options.Builder createOptions() { 17 | Persistent.UpdateReq.Options.Builder optionsBuilder = Persistent.UpdateReq.Options.newBuilder(); 18 | Persistent.UpdateReq.AllOptions.Builder allOptionsBuilder = Persistent.UpdateReq.AllOptions.newBuilder(); 19 | 20 | StreamPosition startFrom = options.getSettings().getStartFrom(); 21 | 22 | if (startFrom instanceof StreamPosition.Start) { 23 | allOptionsBuilder.setStart(Shared.Empty.newBuilder()); 24 | } else if (startFrom instanceof StreamPosition.End) { 25 | allOptionsBuilder.setEnd(Shared.Empty.newBuilder()); 26 | } else { 27 | Position position = startFrom.getPositionOrThrow(); 28 | allOptionsBuilder.setPosition(Persistent.UpdateReq.Position.newBuilder() 29 | .setCommitPosition(position.getCommitUnsigned()) 30 | .setPreparePosition(position.getPrepareUnsigned())); 31 | } 32 | 33 | optionsBuilder.setAll(allOptionsBuilder); 34 | 35 | return optionsBuilder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "author": "Kurrent Inc", 7 | "devDependencies": { 8 | "@babel/cli": "^7.24.8", 9 | "@babel/core": "^7.24.9", 10 | "@babel/preset-env": "^7.24.8", 11 | "@babel/preset-typescript": "^7.24.7", 12 | "@types/fs-extra": "^11.0.4", 13 | "@types/markdown-it": "^14.1.1", 14 | "degit": "2.8.4", 15 | "del": "5.1.0", 16 | "dotenv": "10.0.0", 17 | "eslint": "^8.57.0", 18 | "eslint-config-vuepress": "^4.10.1", 19 | "eslint-config-vuepress-typescript": "^4.10.1", 20 | "fs-extra": "^11.2.0", 21 | "markdown-it": "^14.1.0", 22 | "prettier": "2.3.2", 23 | "sass": "^1.84.0", 24 | "shx": "0.3.3", 25 | "stylus": "^0.56.0", 26 | "tsconfig-vuepress": "^4.5.0", 27 | "typescript": "^5.5.3", 28 | "vite-plugin-vue-devtools": "^7.3.6", 29 | "@vuepress/plugin-search": "^2.0.0-rc.74", 30 | "@mdit/plugin-dl": "^0.13.0" 31 | }, 32 | "dependencies": { 33 | "@rollup/plugin-alias": "^3.1.9", 34 | "@vuelidate/core": "^2.0.3", 35 | "@vuepress/bundler-vite": "2.0.0-rc.19", 36 | "vuepress-theme-hope": "^2.0.0-rc.71", 37 | "sass-loader": "^15.0.0", 38 | "vue": "^3.4.30", 39 | "vue-router": "^4.4.0", 40 | "vuepress": "2.0.0-rc.19", 41 | "iconify-icon": "^2.1.0", 42 | "mermaid": "^11.3.0" 43 | }, 44 | "scripts": { 45 | "preinstall": "npx only-allow pnpm", 46 | "dev": "vuepress-vite dev .", 47 | "build": "vuepress-vite build .", 48 | "update-package": "pnpm dlx vp-update" 49 | }, 50 | "engines": { 51 | "node": ">=18.19.0" 52 | }, 53 | "packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c" 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/DatabaseFactory.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.databases.DockerContainerDatabase; 4 | import io.kurrent.dbclient.databases.ExternallyCreatedCluster; 5 | 6 | import java.util.Optional; 7 | 8 | public class DatabaseFactory { 9 | public static Database spawn() { 10 | boolean secure = Boolean.parseBoolean(Optional.ofNullable(System.getenv("SECURE")).orElse("false")); 11 | boolean cluster = Boolean.parseBoolean(Optional.ofNullable(System.getenv("CLUSTER")).orElse("false")); 12 | 13 | if (cluster) 14 | return new ExternallyCreatedCluster(secure); 15 | 16 | return singleNodeBuilder() 17 | .secure(secure) 18 | .build(); 19 | } 20 | 21 | public static Database spawnEnterpriseWithPluginsEnabled(String... pluginsToEnable) { 22 | boolean secure = Boolean.parseBoolean(Optional.ofNullable(System.getenv("SECURE")).orElse("false")); 23 | boolean cluster = Boolean.parseBoolean(Optional.ofNullable(System.getenv("CLUSTER")).orElse("false")); 24 | 25 | if (cluster) 26 | return new ExternallyCreatedCluster(secure); 27 | 28 | DockerContainerDatabase.Builder builder = singleNodeBuilder(); 29 | 30 | for (String plugin : pluginsToEnable) 31 | builder.env(String.format("EventStore__Plugins__%s__Enabled", plugin), "true"); 32 | 33 | return builder.secure(secure).build(); 34 | } 35 | 36 | private static DockerContainerDatabase.Builder singleNodeBuilder() { 37 | return DockerContainerDatabase 38 | .builder() 39 | .image(Optional 40 | .ofNullable(System.getenv("KURRENTDB_IMAGE")) 41 | .orElse(DockerContainerDatabase.DEFAULT_IMAGE)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/streams/ClientLifecycleTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.streams; 2 | 3 | import io.kurrent.dbclient.ConnectionAware; 4 | import io.kurrent.dbclient.ConnectionShutdownException; 5 | import io.kurrent.dbclient.KurrentDBClient; 6 | import io.kurrent.dbclient.KurrentDBClientSettings; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.concurrent.ExecutionException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertFalse; 12 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | import static org.junit.jupiter.api.Assertions.fail; 15 | 16 | public interface ClientLifecycleTests extends ConnectionAware { 17 | @Test 18 | default void testProvidesRunningStatus() { 19 | KurrentDBClient client = getDatabase().newClient(); 20 | 21 | assertFalse(client.isShutdown()); 22 | } 23 | 24 | @Test 25 | default void testProvidesShutdownStatusAfterManualShutdown() throws Throwable { 26 | KurrentDBClient client = getDatabase().newClient(); 27 | 28 | client.shutdown().get(); 29 | 30 | assertTrue(client.isShutdown()); 31 | } 32 | 33 | @Test 34 | default void testProvidesShutdownStatusAfterAutomaticShutdown() throws Throwable { 35 | KurrentDBClientSettings settings = KurrentDBClientSettings.builder() 36 | .addHost("unknown.host.name", 2113) 37 | .buildConnectionSettings(); 38 | KurrentDBClient client = KurrentDBClient.create(settings); 39 | 40 | try { 41 | client.readAll().get(); 42 | fail(); 43 | } catch (ExecutionException ex) { 44 | assertInstanceOf(ConnectionShutdownException.class, ex.getCause()); 45 | } 46 | assertTrue(client.isShutdown()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/.vuepress/markdown/replaceLink/index.ts: -------------------------------------------------------------------------------- 1 | import {type PluginWithOptions} from "markdown-it"; 2 | import {logger} from "vuepress/utils"; 3 | import {isKnownPlaceholder} from "../../lib/externalPlaceholders"; 4 | import type {MarkdownEnv, MdToken} from "../types"; 5 | 6 | export interface ReplaceLinkPluginOptions { 7 | replaceLink?: (link: string, env: any) => string; 8 | } 9 | 10 | function replaceCrossLinks(token: MdToken, env: MarkdownEnv) { 11 | const href = token.attrGet("href"); 12 | if (href === null) return; 13 | if (!href.startsWith("@")) return; 14 | 15 | const placeholder = href.split("/")[0]; 16 | const known = isKnownPlaceholder(placeholder); 17 | 18 | if (!known) { 19 | logger.error(`Unable to resolve placeholder ${placeholder} in ${href}, file ${env.filePathRelative}`); 20 | return; 21 | } 22 | } 23 | 24 | export const replaceLinkPlugin: PluginWithOptions = (md, opts) => { 25 | md.core.ruler.after( 26 | "inline", 27 | "replace-link", 28 | (state) => { 29 | if (opts?.replaceLink === undefined) return; 30 | state.tokens.forEach((blockToken) => { 31 | if (!(blockToken.type === "inline" && blockToken.children)) { 32 | return; 33 | } 34 | 35 | const replaceAttr = (token: MdToken, attrName: string) => token.attrSet(attrName, opts.replaceLink!(token.attrGet(attrName)!, state.env)); 36 | 37 | blockToken.children.forEach((token) => { 38 | const type = token.type; 39 | if (type === "link_open") { 40 | replaceAttr(token, "href"); 41 | replaceCrossLinks(token, state.env); 42 | } 43 | }); 44 | }); 45 | } 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetProjectionStatus.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | 9 | class GetProjectionStatus { 10 | private final GrpcClient client; 11 | private final String projectionName; 12 | private final GetProjectionStatusOptions options; 13 | 14 | public GetProjectionStatus(final GrpcClient client, final String projectionName, final GetProjectionStatusOptions options) { 15 | this.client = client; 16 | this.projectionName = projectionName; 17 | this.options = options; 18 | } 19 | 20 | public CompletableFuture execute() { 21 | return this.client.run(channel -> { 22 | Projectionmanagement.StatisticsReq.Options.Builder optionsBuilder = 23 | Projectionmanagement.StatisticsReq.Options.newBuilder() 24 | .setName(this.projectionName); 25 | 26 | Projectionmanagement.StatisticsReq request = Projectionmanagement.StatisticsReq.newBuilder() 27 | .setOptions(optionsBuilder) 28 | .build(); 29 | 30 | ProjectionsGrpc.ProjectionsStub client = 31 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 32 | 33 | CompletableFuture result = new CompletableFuture<>(); 34 | 35 | client.statistics(request, GrpcUtils.convertSingleResponse(result, resp -> { 36 | final Projectionmanagement.StatisticsResp.Details details = resp.getDetails(); 37 | return ProjectionDetails.fromWire(details); 38 | })); 39 | 40 | return result; 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AbstractDeletePersistentSubscription.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.persistentsubscriptions.PersistentSubscriptionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | abstract class AbstractDeletePersistentSubscription { 9 | private final GrpcClient client; 10 | private final String group; 11 | private final DeletePersistentSubscriptionOptions options; 12 | 13 | public AbstractDeletePersistentSubscription(GrpcClient client, String group, DeletePersistentSubscriptionOptions options) { 14 | this.client = client; 15 | this.group = group; 16 | this.options = options; 17 | } 18 | 19 | protected abstract Persistent.DeleteReq.Options.Builder createOptions(); 20 | 21 | @SuppressWarnings("unchecked") 22 | public CompletableFuture execute() { 23 | return this.client.runWithArgs(args -> { 24 | CompletableFuture result = new CompletableFuture(); 25 | PersistentSubscriptionsGrpc.PersistentSubscriptionsStub client = 26 | GrpcUtils.configureStub(PersistentSubscriptionsGrpc.newStub(args.getChannel()), this.client.getSettings(), this.options); 27 | 28 | Persistent.DeleteReq req = Persistent.DeleteReq.newBuilder() 29 | .setOptions(createOptions() 30 | .setGroupName(group)) 31 | .build(); 32 | 33 | if (req.getOptions().hasAll() && !args.supportFeature(FeatureFlags.PERSISTENT_SUBSCRIPTION_TO_ALL)) { 34 | result.completeExceptionally(new UnsupportedFeatureException()); 35 | } else { 36 | client.delete(req, GrpcUtils.convertSingleResponse(result)); 37 | } 38 | 39 | return result; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/GetProjectionStatistics.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | 9 | class GetProjectionStatistics { 10 | private final GrpcClient client; 11 | private final String projectionName; 12 | private final GetProjectionStatisticsOptions options; 13 | 14 | public GetProjectionStatistics(final GrpcClient client, final String projectionName, final GetProjectionStatisticsOptions options) { 15 | this.client = client; 16 | this.projectionName = projectionName; 17 | this.options = options; 18 | } 19 | 20 | public CompletableFuture execute() { 21 | return this.client.run(channel -> { 22 | Projectionmanagement.StatisticsReq.Options.Builder optionsBuilder = 23 | Projectionmanagement.StatisticsReq.Options.newBuilder() 24 | .setName(this.projectionName); 25 | 26 | Projectionmanagement.StatisticsReq request = Projectionmanagement.StatisticsReq.newBuilder() 27 | .setOptions(optionsBuilder) 28 | .build(); 29 | 30 | ProjectionsGrpc.ProjectionsStub client = 31 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 32 | 33 | CompletableFuture result = new CompletableFuture<>(); 34 | 35 | client.statistics(request, GrpcUtils.convertSingleResponse(result, resp -> { 36 | final Projectionmanagement.StatisticsResp.Details details = resp.getDetails(); 37 | return ProjectionDetails.fromWire(details); 38 | })); 39 | 40 | return result; 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/misc/ServerVersionTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.misc; 2 | 3 | import io.kurrent.dbclient.ServerVersion; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class ServerVersionTests { 8 | @Test 9 | public void testEquals() { 10 | ServerVersion v = new ServerVersion(22, 6, 123); 11 | 12 | Assertions.assertTrue(v.equals(22, 6, 123)); 13 | 14 | Assertions.assertFalse(v.equals(21, 6, 123)); 15 | Assertions.assertFalse(v.equals(22, 5, 123)); 16 | Assertions.assertFalse(v.equals(22, 6, 124)); 17 | } 18 | 19 | @Test 20 | public void testIsLessThan() { 21 | ServerVersion v = new ServerVersion(22, 6, 123); 22 | 23 | Assertions.assertFalse(v.isLessThan(22, 6, 123)); 24 | 25 | Assertions.assertTrue(v.isLessThan(23, 6, 123)); 26 | Assertions.assertTrue(v.isLessThan(22, 7, 123)); 27 | Assertions.assertTrue(v.isLessThan(22, 6, 124)); 28 | 29 | Assertions.assertFalse(v.isLessThan(21, 6, 123)); 30 | Assertions.assertFalse(v.isLessThan(22, 5, 123)); 31 | Assertions.assertFalse(v.isLessThan(22, 6, 122)); 32 | 33 | Assertions.assertTrue(v.isLessThan(23, 5, 123)); 34 | } 35 | 36 | @Test 37 | public void testIsGreaterThan() { 38 | ServerVersion v = new ServerVersion(22, 6, 123); 39 | 40 | Assertions.assertFalse(v.isGreaterThan(22, 6, 123)); 41 | 42 | Assertions.assertTrue(v.isGreaterThan(21, 6, 123)); 43 | Assertions.assertTrue(v.isGreaterThan(22, 5, 123)); 44 | Assertions.assertTrue(v.isGreaterThan(22, 6, 122)); 45 | 46 | Assertions.assertFalse(v.isGreaterThan(23, 6, 123)); 47 | Assertions.assertFalse(v.isGreaterThan(22, 7, 123)); 48 | Assertions.assertFalse(v.isGreaterThan(22, 6, 124)); 49 | 50 | Assertions.assertTrue(v.isGreaterThan(21, 7, 123)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/DeleteProjection.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | class DeleteProjection { 9 | private final GrpcClient client; 10 | private final String projectionName; 11 | private final DeleteProjectionOptions options; 12 | 13 | public DeleteProjection(final GrpcClient client, final String projectionName, final DeleteProjectionOptions options) { 14 | this.client = client; 15 | this.projectionName = projectionName; 16 | this.options = options; 17 | } 18 | 19 | public CompletableFuture execute() { 20 | return this.client.run(channel -> { 21 | Projectionmanagement.DeleteReq.Options reqOptions = 22 | Projectionmanagement.DeleteReq.Options.newBuilder() 23 | .setName(this.projectionName) 24 | .setDeleteCheckpointStream(options.getDeleteCheckpointStream()) 25 | .setDeleteEmittedStreams(options.getDeleteEmittedStreams()) 26 | .setDeleteStateStream(options.getDeleteStateStream()) 27 | .build(); 28 | 29 | Projectionmanagement.DeleteReq request = Projectionmanagement.DeleteReq.newBuilder() 30 | .setOptions(reqOptions) 31 | .build(); 32 | 33 | ProjectionsGrpc.ProjectionsStub client = 34 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 35 | 36 | CompletableFuture result = new CompletableFuture<>(); 37 | 38 | client.delete(request, GrpcUtils.convertSingleResponse(result)); 39 | 40 | return result; 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadStreamConsumer.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import org.reactivestreams.Subscriber; 4 | 5 | import java.time.Instant; 6 | 7 | class ReadStreamConsumer implements StreamConsumer { 8 | private final Subscriber subscriber; 9 | 10 | public ReadStreamConsumer(Subscriber subscriber) { 11 | this.subscriber = subscriber; 12 | } 13 | 14 | @Override 15 | public void onEvent(ResolvedEvent event) { 16 | this.subscriber.onNext(ReadMessage.fromEvent(event)); 17 | } 18 | 19 | @Override 20 | public void onSubscriptionConfirmation(String subscriptionId) {} 21 | 22 | @Override 23 | public void onCheckpoint(long commit, long prepare) {} 24 | 25 | @Override 26 | public void onStreamNotFound(String streamName) { 27 | this.subscriber.onError(new StreamNotFoundException(streamName)); 28 | } 29 | 30 | @Override 31 | public void onFirstStreamPosition(long position) { 32 | this.subscriber.onNext(ReadMessage.fromFirstStreamPosition(position)); 33 | } 34 | 35 | @Override 36 | public void onLastStreamPosition(long position) { 37 | this.subscriber.onNext(ReadMessage.fromLastStreamPosition(position)); 38 | } 39 | 40 | @Override 41 | public void onLastAllStreamPosition(long commit, long prepare) { 42 | this.subscriber.onNext(ReadMessage.fromLastAllPosition(commit, prepare)); 43 | } 44 | 45 | @Override 46 | public void onCaughtUp(Instant timestamp, Long streamRevision, Position position) {} 47 | 48 | @Override 49 | public void onFellBehind(Instant timestamp, Long streamRevision, Position position) {} 50 | 51 | @Override 52 | public void onCancelled(Throwable exception) { 53 | this.subscriber.onError(exception); 54 | } 55 | 56 | @Override 57 | public void onComplete() { 58 | this.subscriber.onComplete(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/misc/OfflineMetadataTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.misc; 2 | 3 | import io.kurrent.dbclient.Acl; 4 | import io.kurrent.dbclient.Acls; 5 | import io.kurrent.dbclient.StreamMetadata; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.HashMap; 11 | 12 | public class OfflineMetadataTests { 13 | @Test 14 | public void testSerializationIsoMorphism() throws Throwable { 15 | StreamMetadata expected = new StreamMetadata(); 16 | HashMap custom = new HashMap<>(); 17 | 18 | custom.put("foo", "bar"); 19 | 20 | expected.setMaxAge(2L); 21 | expected.setCacheControl(15L); 22 | expected.setTruncateBefore(1L); 23 | expected.setMaxCount(12L); 24 | 25 | Acl acl = Acls.newStreamAcl() 26 | .addReadRoles("admin") 27 | .addWriteRoles("admin") 28 | .addDeleteRoles("admin") 29 | .addMetaReadRoles("admin") 30 | .addMetaWriteRoles("admin"); 31 | 32 | expected.setAcl(acl); 33 | expected.setCustomProperties(custom); 34 | 35 | ObjectMapper mapper = new ObjectMapper(); 36 | StreamMetadata actual = mapper.readValue(mapper.writeValueAsString(expected), StreamMetadata.class); 37 | 38 | Assertions.assertEquals(expected, actual); 39 | } 40 | 41 | @Test 42 | public void testNullAcl() throws Throwable { 43 | StreamMetadata expected = new StreamMetadata(); 44 | 45 | expected.setMaxAge(2L); 46 | expected.setCacheControl(15L); 47 | expected.setTruncateBefore(1L); 48 | expected.setMaxCount(12L); 49 | 50 | ObjectMapper mapper = new ObjectMapper(); 51 | StreamMetadata actual = mapper.readValue(mapper.writeValueAsString(expected), StreamMetadata.class); 52 | 53 | Assertions.assertEquals(expected, actual); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/api/delete-stream.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 9 3 | head: 4 | - - title 5 | - {} 6 | - Deleting Events | Java | Clients | Kurrent Docs 7 | --- 8 | 9 | # Deleting Events 10 | 11 | In KurrentDB, you can delete events and streams either partially or 12 | completely. Settings like $maxAge and $maxCount help control how long events are 13 | kept or how many events are stored in a stream, but they won't delete the entire 14 | stream. When you need to fully remove a stream, KurrentDB offers two 15 | options: Soft Delete and Hard Delete. 16 | 17 | ## Soft delete 18 | 19 | Soft delete in KurrentDB allows you to mark a stream for deletion without 20 | completely removing it, so you can still add new events later. While you can do 21 | this through the UI, using code is often better for automating the process, 22 | handling many streams at once, or including custom rules. Code is especially 23 | helpful for large-scale deletions or when you need to integrate soft deletes 24 | into other workflows. 25 | 26 | ```java 27 | client.deleteStream(streamName, DeleteStreamOptions.get()).get(); 28 | ``` 29 | 30 | ::: note 31 | Clicking the delete button in the UI performs a soft delete, setting the 32 | TruncateBefore value to remove all events up to a certain point. While this 33 | marks the events for deletion, actual removal occurs during the next scavenging 34 | process. The stream can still be reopened by appending new events. 35 | ::: 36 | 37 | ## Hard delete 38 | 39 | Hard delete in KurrentDB permanently removes a stream and its events. While 40 | you can use the HTTP API, code is often better for automating the process, 41 | managing multiple streams, and ensuring precise control. Code is especially 42 | useful when you need to integrate hard delete into larger workflows or apply 43 | specific conditions. Note that when a stream is hard deleted, you cannot reuse 44 | the stream name, it will raise an exception if you try to append to it again. 45 | 46 | ```java 47 | client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); 48 | ``` -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadResult.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Returned after a successful read operation. 7 | */ 8 | public class ReadResult { 9 | private final List events; 10 | private final long firstStreamPosition; 11 | private final long lastStreamPosition; 12 | private final Position lastAllStreamPosition; 13 | 14 | ReadResult(List events, long firstStreamPosition, long lastStreamPosition, Position lastAllStreamPosition) { 15 | this.events = events; 16 | this.firstStreamPosition = firstStreamPosition; 17 | this.lastStreamPosition = lastStreamPosition; 18 | this.lastAllStreamPosition = lastAllStreamPosition; 19 | } 20 | 21 | /** 22 | * Returns all the events of the read operation. 23 | */ 24 | public List getEvents() { 25 | return this.events; 26 | } 27 | 28 | /** 29 | * When reading from a regular stream, returns the first event revision number of the stream. 30 | */ 31 | public long getFirstStreamPosition() { 32 | return firstStreamPosition; 33 | } 34 | 35 | /** 36 | * When reading from a regular stream, returns the last event revision number of the stream. 37 | */ 38 | public long getLastStreamPosition() { 39 | return lastStreamPosition; 40 | } 41 | 42 | /** 43 | * When reading from $all stream, returns the last event position. 44 | * @return null if reading from a regular stream. 45 | */ 46 | public Position getLastAllStreamPosition() { 47 | return lastAllStreamPosition; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "ReadResult{" + 53 | "events=" + events + 54 | ", firstStreamPosition=" + firstStreamPosition + 55 | ", lastStreamPosition=" + lastStreamPosition + 56 | ", lastAllStreamPosition=" + lastAllStreamPosition + 57 | '}'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/streams/DeleteTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.streams; 2 | 3 | import io.kurrent.dbclient.*; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.concurrent.ExecutionException; 8 | 9 | public interface DeleteTests extends ConnectionAware { 10 | @Test 11 | default void testCanDeleteStream() throws Throwable { 12 | KurrentDBClient client = getDatabase().defaultClient(); 13 | String streamName = generateName(); 14 | 15 | client.appendToStream(streamName, generateEvents(1, "foobar").iterator()).get(); 16 | 17 | client.deleteStream(streamName).get(); 18 | } 19 | 20 | @Test 21 | default void testDeleteStreamWhenAlreadyDeleted() throws Throwable { 22 | KurrentDBClient client = getDatabase().defaultClient(); 23 | String streamName = generateName(); 24 | 25 | client.appendToStream(streamName, generateEvents(1, "foobar").iterator()).get(); 26 | client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); 27 | Assertions.assertThrows(StreamDeletedException.class, () -> { 28 | try { 29 | client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); 30 | } catch (ExecutionException e) { 31 | throw e.getCause(); 32 | } 33 | }); 34 | } 35 | 36 | @Test 37 | default void testDeleteStreamWhenDoesntExist() throws Throwable { 38 | KurrentDBClient client = getDatabase().defaultClient(); 39 | 40 | String streamName = generateName(); 41 | DeleteStreamOptions options = DeleteStreamOptions.get() 42 | .streamState(StreamState.streamExists()); 43 | 44 | Assertions.assertThrows(WrongExpectedVersionException.class, () -> { 45 | try { 46 | client.tombstoneStream(streamName, options).get(); 47 | } catch (ExecutionException e) { 48 | throw e.getCause(); 49 | } 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/AbstractRead.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.shared.Shared; 4 | import io.kurrent.dbclient.proto.streams.StreamsGrpc; 5 | import io.kurrent.dbclient.proto.streams.StreamsOuterClass; 6 | import org.reactivestreams.Publisher; 7 | import org.reactivestreams.Subscriber; 8 | 9 | abstract class AbstractRead implements Publisher { 10 | protected static final StreamsOuterClass.ReadReq.Options.Builder defaultReadOptions; 11 | 12 | private final GrpcClient client; 13 | private final OptionsWithBackPressure options; 14 | 15 | protected AbstractRead(GrpcClient client, OptionsWithBackPressure options) { 16 | this.client = client; 17 | this.options = options; 18 | } 19 | 20 | static { 21 | defaultReadOptions = StreamsOuterClass.ReadReq.Options.newBuilder() 22 | .setUuidOption(StreamsOuterClass.ReadReq.Options.UUIDOption.newBuilder() 23 | .setStructured(Shared.Empty.getDefaultInstance())); 24 | } 25 | 26 | public abstract StreamsOuterClass.ReadReq.Options.Builder createOptions(); 27 | 28 | @Override 29 | public void subscribe(Subscriber subscriber) { 30 | ReadResponseObserver observer = new ReadResponseObserver(options, new ReadStreamConsumer(subscriber)); 31 | 32 | this.client.getWorkItemArgs().whenComplete((args, error) -> { 33 | if (error != null) { 34 | observer.onError(error); 35 | return; 36 | } 37 | 38 | StreamsOuterClass.ReadReq request = StreamsOuterClass.ReadReq.newBuilder() 39 | .setOptions(createOptions()) 40 | .build(); 41 | 42 | StreamsGrpc.StreamsStub client = GrpcUtils.configureStub(StreamsGrpc.newStub(args.getChannel()), this.client.getSettings(), this.options); 43 | observer.onConnected(args); 44 | subscriber.onSubscribe(observer.getSubscription()); 45 | client.read(request, observer); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/api/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authentication 3 | order: 7 4 | head: 5 | - - title 6 | - {} 7 | - Authentication | Java | Clients | Kurrent Docs 8 | --- 9 | 10 | # Client x.509 certificate 11 | 12 | 13 | 14 | X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. 15 | 16 | ## Prerequisites 17 | 18 | 1. KurrentDB 25.0 or greater, or KurrentDB 24.10 or later. 19 | 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. 20 | 21 | ## Connect using an x.509 certificate 22 | 23 | To connect using an x.509 certificate, you need to provide the certificate and 24 | the private key to the client. If both username or password and certificate 25 | authentication data are supplied, the client prioritizes user credentials for 26 | authentication. The client will throw an error if the certificate and the key 27 | are not both provided. 28 | 29 | The client supports the following parameters: 30 | 31 | | Parameter | Description | 32 | |----------------|--------------------------------------------------------------------------------| 33 | | `userCertFile` | The file containing the X.509 user certificate in PEM format. | 34 | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | 35 | 36 | To authenticate, include these two parameters in your connection string or constructor when initializing the client: 37 | 38 | ```java 39 | KurrentDBClientSettings settings = KurrentDBConnectionString 40 | .parseOrThrow("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}"); 41 | 42 | KurrentDBClient client = KurrentDBClient.create(settings); 43 | ``` -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/streams/ReadStreamTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.streams; 2 | 3 | import io.kurrent.dbclient.*; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.ExecutionException; 10 | 11 | public interface ReadStreamTests extends ConnectionAware { 12 | @Test 13 | default void testReadStreamEvents() throws Throwable { 14 | String streamName = generateName(); 15 | int count = 3; 16 | List expecteds = generateBazEvent(count); 17 | List actuals = new ArrayList<>(); 18 | List events = new ArrayList<>(); 19 | 20 | for (BazEvent event : expecteds) { 21 | events.add(serializeBazEvent(event)); 22 | } 23 | 24 | KurrentDBClient client = getDefaultClient(); 25 | 26 | client.appendToStream(streamName, events.iterator()).get(); 27 | ReadResult result = client.readStream(streamName, ReadStreamOptions.get()).get(); 28 | 29 | for (ResolvedEvent resolvedEvent : result.getEvents()) { 30 | BazEvent event = deserializeBazEvent(resolvedEvent.getOriginalEvent().getEventData()); 31 | actuals.add(event); 32 | } 33 | 34 | for (int i = 0; i < count; i++) { 35 | BazEvent actual = actuals.get(i); 36 | BazEvent expected = expecteds.get(i); 37 | Assertions.assertEquals(expected.getName(), actual.getName()); 38 | Assertions.assertEquals(expected.getAge(), actual.getAge()); 39 | } 40 | } 41 | 42 | @Test 43 | default void testNonexistentStream() throws Throwable { 44 | String streamName = generateName(); 45 | 46 | Assertions.assertThrows(StreamNotFoundException.class, () -> { 47 | try { 48 | getDefaultClient().readStream(streamName, ReadStreamOptions.get()).get(); 49 | } catch (ExecutionException e) { 50 | throw e.getCause(); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/SubscribeToAll.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.shared.Shared; 4 | import io.kurrent.dbclient.proto.streams.StreamsOuterClass; 5 | 6 | class SubscribeToAll extends AbstractRegularSubscription { 7 | private final SubscribeToAllOptions options; 8 | 9 | public SubscribeToAll(GrpcClient client, SubscriptionListener listener, SubscribeToAllOptions options) { 10 | super(client, options); 11 | 12 | this.options = options; 13 | this.listener = listener; 14 | } 15 | 16 | @Override 17 | protected StreamsOuterClass.ReadReq.Options.Builder createOptions() { 18 | StreamsOuterClass.ReadReq.Options.AllOptions.Builder allOptions = StreamsOuterClass.ReadReq.Options.AllOptions.newBuilder(); 19 | 20 | if (this.options.getPosition().isEnd()) { 21 | allOptions.setEnd(Shared.Empty.getDefaultInstance()); 22 | } else if (this.options.getPosition().isStart()) { 23 | allOptions.setStart(Shared.Empty.getDefaultInstance()); 24 | } else { 25 | StreamPosition.Position position = (StreamPosition.Position) this.options.getPosition(); 26 | allOptions.setPosition(StreamsOuterClass.ReadReq.Options.Position.newBuilder() 27 | .setCommitPosition(position.getPositionOrThrow().getCommitUnsigned()) 28 | .setPreparePosition(position.getPositionOrThrow().getPrepareUnsigned())); 29 | } 30 | StreamsOuterClass.ReadReq.Options.Builder options = 31 | defaultSubscribeOptions.clone() 32 | .setResolveLinks(this.options.shouldResolveLinkTos()) 33 | .setAll(allOptions); 34 | 35 | if (this.options.getFilter() != null) { 36 | this.options.getFilter().addToWireStreamsReadReq(options); 37 | this.checkpointer = this.options.getFilter().getCheckpointer(); 38 | } else { 39 | options.setNoFilter(Shared.Empty.getDefaultInstance()); 40 | } 41 | 42 | return options; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/CreatePersistentSubscriptionToAll.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.Persistent; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | 6 | class CreatePersistentSubscriptionToAll extends AbstractCreatePersistentSubscription { 7 | private final CreatePersistentSubscriptionToAllOptions options; 8 | 9 | public CreatePersistentSubscriptionToAll(GrpcClient client, String group, 10 | CreatePersistentSubscriptionToAllOptions options) { 11 | super(client, group, options.getSettings(), options); 12 | this.options = options; 13 | } 14 | 15 | @Override 16 | protected Persistent.CreateReq.Options.Builder createOptions() { 17 | Persistent.CreateReq.Options.Builder optionsBuilder = Persistent.CreateReq.Options.newBuilder(); 18 | Persistent.CreateReq.AllOptions.Builder allOptionsBuilder = Persistent.CreateReq.AllOptions.newBuilder(); 19 | StreamPosition position = this.options.getSettings().getStartFrom(); 20 | 21 | if (position instanceof StreamPosition.Start) { 22 | allOptionsBuilder.setStart(Shared.Empty.newBuilder()); 23 | } else if (position instanceof StreamPosition.End) { 24 | allOptionsBuilder.setEnd(Shared.Empty.newBuilder()); 25 | } else { 26 | Position pos = position.getPositionOrThrow(); 27 | allOptionsBuilder.setPosition(Persistent.CreateReq.Position.newBuilder() 28 | .setCommitPosition(pos.getCommitUnsigned()) 29 | .setPreparePosition(pos.getPrepareUnsigned())); 30 | } 31 | 32 | SubscriptionFilter filter = options.getFilter(); 33 | if (filter != null) { 34 | filter.addToWirePersistentCreateReq(allOptionsBuilder); 35 | } else { 36 | allOptionsBuilder.setNoFilter(Shared.Empty.getDefaultInstance()); 37 | } 38 | 39 | optionsBuilder.setAll(allOptionsBuilder); 40 | 41 | return optionsBuilder; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UpdatePersistentSubscriptionToStreamOptions.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | /** 4 | * Options of the update persistent subscription to stream request. 5 | */ 6 | public class UpdatePersistentSubscriptionToStreamOptions 7 | extends AbstractPersistentSubscriptionSettingsBuilder { 8 | UpdatePersistentSubscriptionToStreamOptions() { 9 | this(PersistentSubscriptionSettings.defaultRegular()); 10 | } 11 | 12 | UpdatePersistentSubscriptionToStreamOptions(PersistentSubscriptionToStreamSettings settings) { 13 | super(settings); 14 | } 15 | 16 | /** 17 | * Returns options with default values. 18 | */ 19 | public static UpdatePersistentSubscriptionToStreamOptions get() { 20 | return new UpdatePersistentSubscriptionToStreamOptions(); 21 | } 22 | 23 | /** 24 | * Returns options from a persistent subscription to stream settings. 25 | * @see PersistentSubscriptionToStreamSettings 26 | */ 27 | public static UpdatePersistentSubscriptionToStreamOptions from(PersistentSubscriptionToStreamSettings settings) { 28 | return new UpdatePersistentSubscriptionToStreamOptions(settings); 29 | } 30 | 31 | /** 32 | * Starts the persistent subscription from the beginning of the stream. 33 | */ 34 | public UpdatePersistentSubscriptionToStreamOptions fromStart() { 35 | getSettings().setStartFrom(StreamPosition.start()); 36 | return this; 37 | } 38 | 39 | /** 40 | * Starts the persistent subscription from the end of the stream. 41 | */ 42 | public UpdatePersistentSubscriptionToStreamOptions fromEnd() { 43 | getSettings().setStartFrom(StreamPosition.end()); 44 | return this; 45 | } 46 | 47 | /** 48 | * Starts the persistent subscription from a specific revision number. 49 | */ 50 | public UpdatePersistentSubscriptionToStreamOptions startFrom(long revision) { 51 | getSettings().setStartFrom(StreamPosition.position(revision)); 52 | return this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/RestartPersistentSubscriptionSubsystem.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.persistentsubscriptions.PersistentSubscriptionsGrpc; 4 | import io.kurrent.dbclient.proto.shared.Shared; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | import static io.kurrent.dbclient.HttpUtils.checkForError; 11 | 12 | class RestartPersistentSubscriptionSubsystem { 13 | @SuppressWarnings("unchecked") 14 | public static CompletableFuture execute(GrpcClient client, RestartPersistentSubscriptionSubsystemOptions options) { 15 | return client.runWithArgs(args -> { 16 | CompletableFuture result = new CompletableFuture(); 17 | 18 | if (args.supportFeature(FeatureFlags.PERSISTENT_SUBSCRIPTION_MANAGEMENT)) { 19 | PersistentSubscriptionsGrpc.PersistentSubscriptionsStub stub = 20 | GrpcUtils.configureStub(PersistentSubscriptionsGrpc.newStub(args.getChannel()), client.getSettings(), options); 21 | 22 | stub.restartSubsystem(Shared.Empty.getDefaultInstance(), GrpcUtils.convertSingleResponse(result, resp -> 42)); 23 | } else { 24 | HttpURLConnection http = args.getHttpConnection(options, client.getSettings(), "/subscriptions/restart"); 25 | 26 | try { 27 | http.setDoOutput(true); 28 | http.setRequestMethod("POST"); 29 | http.setFixedLengthStreamingMode(0); 30 | 31 | Exception error = checkForError(http.getResponseCode()); 32 | if (error != null) { 33 | result.completeExceptionally(error); 34 | } else { 35 | result.complete(42); 36 | } 37 | } catch (IOException e) { 38 | throw new RuntimeException(e); 39 | } finally { 40 | http.disconnect(); 41 | } 42 | } 43 | 44 | return result; 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/ReadAll.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.shared.Shared; 4 | import io.kurrent.dbclient.proto.streams.StreamsOuterClass; 5 | 6 | class ReadAll extends AbstractRead { 7 | private final ReadAllOptions options; 8 | 9 | public ReadAll(GrpcClient client, ReadAllOptions options) { 10 | super(client, options); 11 | 12 | this.options = options; 13 | } 14 | 15 | @Override 16 | public StreamsOuterClass.ReadReq.Options.Builder createOptions() { 17 | StreamsOuterClass.ReadReq.Options.AllOptions.Builder optionsOrBuilder = 18 | StreamsOuterClass.ReadReq.Options.AllOptions.newBuilder(); 19 | 20 | if (this.options.getPosition().isEnd()) { 21 | optionsOrBuilder.setEnd(Shared.Empty.getDefaultInstance()); 22 | } else if (this.options.getPosition().isStart()) { 23 | optionsOrBuilder.setStart(Shared.Empty.getDefaultInstance()); 24 | } else { 25 | StreamPosition.Position position = (StreamPosition.Position) this.options.getPosition(); 26 | optionsOrBuilder.setPosition(StreamsOuterClass.ReadReq.Options.Position.newBuilder() 27 | .setCommitPosition(position.getPositionOrThrow().getCommitUnsigned()) 28 | .setPreparePosition(position.getPositionOrThrow().getPrepareUnsigned())); 29 | } 30 | 31 | StreamsOuterClass.ReadReq.Options.Builder builder = defaultReadOptions.clone() 32 | .setAll(optionsOrBuilder) 33 | .setResolveLinks(this.options.shouldResolveLinkTos()) 34 | .setControlOption(StreamsOuterClass.ReadReq.Options.ControlOption.newBuilder().setCompatibility(1)) 35 | .setCount(this.options.getMaxCount()) 36 | .setNoFilter(Shared.Empty.getDefaultInstance()) 37 | .setReadDirection(this.options.getDirection() == Direction.Forwards ? 38 | StreamsOuterClass.ReadReq.Options.ReadDirection.Forwards : 39 | StreamsOuterClass.ReadReq.Options.ReadDirection.Backwards); 40 | 41 | return builder; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/StreamFilter.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Arrays; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | 8 | class StreamFilter implements EventFilter { 9 | private final PrefixFilterExpression[] prefixFilterExpressions; 10 | private final RegularFilterExpression regularFilterExpression; 11 | @NotNull 12 | private final Optional maxSearchWindow; 13 | 14 | public StreamFilter(@NotNull Optional maxSearchWindow, RegularFilterExpression regex) { 15 | this.maxSearchWindow = maxSearchWindow; 16 | this.regularFilterExpression = regex; 17 | this.prefixFilterExpressions = null; 18 | } 19 | 20 | public StreamFilter(@NotNull Optional maxSearchWindow, PrefixFilterExpression... prefixes) { 21 | this.maxSearchWindow = maxSearchWindow; 22 | this.prefixFilterExpressions = prefixes; 23 | this.regularFilterExpression = null; 24 | } 25 | 26 | @Override 27 | public PrefixFilterExpression[] getPrefixFilterExpressions() { 28 | return this.prefixFilterExpressions; 29 | } 30 | 31 | @Override 32 | public RegularFilterExpression getRegularFilterExpression() { 33 | return regularFilterExpression; 34 | } 35 | 36 | @Override 37 | public Optional getMaxSearchWindow() { 38 | return maxSearchWindow; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | StreamFilter that = (StreamFilter) o; 46 | return Arrays.equals(prefixFilterExpressions, that.prefixFilterExpressions) && 47 | Objects.equals(regularFilterExpression, that.regularFilterExpression) && 48 | maxSearchWindow.equals(that.maxSearchWindow); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | int result = Objects.hash(regularFilterExpression, maxSearchWindow); 54 | result = 31 * result + Arrays.hashCode(prefixFilterExpressions); 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/UpdateProjection.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import io.kurrent.dbclient.proto.projections.Projectionmanagement; 4 | import io.kurrent.dbclient.proto.projections.ProjectionsGrpc; 5 | import io.kurrent.dbclient.proto.shared.Shared; 6 | 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | class UpdateProjection { 10 | private final GrpcClient client; 11 | private final String projectionName; 12 | private final String query; 13 | private final Boolean emitEnabled; 14 | private final UpdateProjectionOptions options; 15 | 16 | public UpdateProjection(final GrpcClient client, final String projectionName, final String query, 17 | final UpdateProjectionOptions options) { 18 | this.client = client; 19 | this.projectionName = projectionName; 20 | this.query = query; 21 | this.emitEnabled = options.isEmitEnabled(); 22 | this.options = options; 23 | } 24 | 25 | public CompletableFuture execute() { 26 | return this.client.run(channel -> { 27 | Projectionmanagement.UpdateReq.Options.Builder optionsBuilder = 28 | Projectionmanagement.UpdateReq.Options.newBuilder() 29 | .setName(this.projectionName) 30 | .setQuery(this.query); 31 | 32 | if(this.emitEnabled == null){ 33 | optionsBuilder.setNoEmitOptions(Shared.Empty.newBuilder()); 34 | } else { 35 | optionsBuilder.setEmitEnabled(this.emitEnabled); 36 | } 37 | 38 | Projectionmanagement.UpdateReq request = Projectionmanagement.UpdateReq.newBuilder() 39 | .setOptions(optionsBuilder) 40 | .build(); 41 | 42 | ProjectionsGrpc.ProjectionsStub client = 43 | GrpcUtils.configureStub(ProjectionsGrpc.newStub(channel), this.client.getSettings(), this.options); 44 | 45 | CompletableFuture result = new CompletableFuture<>(); 46 | 47 | client.update(request, GrpcUtils.convertSingleResponse(result)); 48 | 49 | return result; 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/EventTypeFilter.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Arrays; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | 8 | class EventTypeFilter implements EventFilter { 9 | private final PrefixFilterExpression[] prefixFilterExpressions; 10 | private final RegularFilterExpression regularFilterExpression; 11 | @NotNull 12 | private final Optional maxSearchWindow; 13 | 14 | EventTypeFilter(@NotNull Optional maxSearchWindow, RegularFilterExpression regex) { 15 | this.maxSearchWindow = maxSearchWindow; 16 | this.regularFilterExpression = regex; 17 | this.prefixFilterExpressions = null; 18 | } 19 | 20 | EventTypeFilter(@NotNull Optional maxSearchWindow, PrefixFilterExpression... prefixes) { 21 | this.maxSearchWindow = maxSearchWindow; 22 | this.prefixFilterExpressions = prefixes; 23 | this.regularFilterExpression = null; 24 | } 25 | 26 | @Override 27 | public PrefixFilterExpression[] getPrefixFilterExpressions() { 28 | return this.prefixFilterExpressions; 29 | } 30 | 31 | @Override 32 | public RegularFilterExpression getRegularFilterExpression() { 33 | return regularFilterExpression; 34 | } 35 | 36 | @Override 37 | public Optional getMaxSearchWindow() { 38 | return maxSearchWindow; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | EventTypeFilter that = (EventTypeFilter) o; 46 | return Arrays.equals(prefixFilterExpressions, that.prefixFilterExpressions) && 47 | Objects.equals(regularFilterExpression, that.regularFilterExpression) && 48 | maxSearchWindow.equals(that.maxSearchWindow); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | int result = Objects.hash(regularFilterExpression, maxSearchWindow); 54 | result = 31 * result + Arrays.hashCode(prefixFilterExpressions); 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/samples/quick_start/QuickStart.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.samples.quick_start; 2 | 3 | import io.kurrent.dbclient.*; 4 | import io.kurrent.dbclient.samples.TestEvent; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.json.JsonMapper; 7 | 8 | import java.util.UUID; 9 | import java.util.concurrent.ExecutionException; 10 | 11 | public class QuickStart { 12 | public static void Run() throws ConnectionStringParsingException, ExecutionException, InterruptedException, JsonProcessingException { 13 | // region createClient 14 | KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("{connectionString}"); 15 | KurrentDBClient client = KurrentDBClient.create(settings); 16 | // endregion createClient 17 | 18 | // region createEvent 19 | TestEvent event = new TestEvent(); 20 | JsonMapper jsonMapper = new JsonMapper(); 21 | event.setId(UUID.randomUUID().toString()); 22 | event.setImportantData("I wrote my first event!"); 23 | 24 | EventData eventData = EventData 25 | .builderAsJson("TestEvent", jsonMapper.writeValueAsBytes(event)) 26 | .build(); 27 | // endregion createEvent 28 | 29 | // region appendEvents 30 | client.appendToStream("some-stream", eventData) 31 | .get(); 32 | // endregion appendEvents 33 | 34 | // region overriding-user-credentials 35 | AppendToStreamOptions appendToStreamOptions = AppendToStreamOptions.get() 36 | .authenticated("admin", "changeit"); 37 | 38 | client.appendToStream("some-stream", appendToStreamOptions, eventData) 39 | .get(); 40 | // endregion overriding-user-credentials 41 | 42 | 43 | // region readStream 44 | ReadStreamOptions options = ReadStreamOptions.get() 45 | .forwards() 46 | .fromStart() 47 | .maxCount(10); 48 | 49 | ReadResult result = client.readStream("some-stream", options) 50 | .get(); 51 | // endregion readStream 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/kurrent/dbclient/PersistentSubscriptionToStreamStats.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | * Processing-related persistent subscription to stream statistics. 7 | */ 8 | public class PersistentSubscriptionToStreamStats extends PersistentSubscriptionStats { 9 | private Long lastCheckpointedEventRevision; 10 | private Long lastKnownEventRevision; 11 | 12 | /** 13 | * The revision number of the last checkpoint. 14 | */ 15 | public Optional getLastCheckpointedEventRevision() { 16 | if (lastCheckpointedEventRevision == null) 17 | return Optional.empty(); 18 | 19 | return Optional.of(lastCheckpointedEventRevision); 20 | } 21 | 22 | void setLastCheckpointedEventRevision(long lastCheckpointedEventRevision) { 23 | this.lastCheckpointedEventRevision = lastCheckpointedEventRevision; 24 | } 25 | 26 | /** 27 | * The revision number of the last known event. 28 | */ 29 | public Optional getLastKnownEventRevision() { 30 | if (lastKnownEventRevision == null) 31 | return Optional.empty(); 32 | 33 | return Optional.of(lastKnownEventRevision); 34 | } 35 | 36 | void setLastKnownEventRevision(long lastKnownEventRevision) { 37 | this.lastKnownEventRevision = lastKnownEventRevision; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "PersistentSubscriptionToStreamStats{" + 43 | "lastCheckpointedEventRevision=" + lastCheckpointedEventRevision + 44 | ", lastKnownEventRevision=" + lastKnownEventRevision + 45 | ", averagePerSecond=" + getAveragePerSecond() + 46 | ", totalItems=" + getTotalItems() + 47 | ", countSinceLastMeasurement=" + getCountSinceLastMeasurement() + 48 | ", readBufferCount=" + getReadBufferCount() + 49 | ", liveBufferCount=" + getLiveBufferCount() + 50 | ", retryBufferCount=" + getRetryBufferCount() + 51 | ", totalInFlightMessages=" + getTotalInFlightMessages() + 52 | ", outstandingMessagesCount=" + getOutstandingMessagesCount() + 53 | ", parkedMessageCount=" + getParkedMessageCount() + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/io/kurrent/dbclient/misc/ParseInvalidConnectionStringTests.java: -------------------------------------------------------------------------------- 1 | package io.kurrent.dbclient.misc; 2 | 3 | import io.kurrent.dbclient.ConnectionStringParsingException; 4 | import io.kurrent.dbclient.KurrentDBConnectionString; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.Arguments; 8 | import org.junit.jupiter.params.provider.MethodSource; 9 | 10 | import java.util.stream.Stream; 11 | 12 | public class ParseInvalidConnectionStringTests { 13 | public static Stream invalidConnectionStrings() { 14 | return Stream.of( 15 | Arguments.of("localhost"), 16 | Arguments.of("https://console.eventstore.cloud/"), 17 | Arguments.of("kurnt+discovery://localhost"), 18 | Arguments.of("kurrent://my:great@username:UyeXx8$^PsOo4jG88FlCauR1Coz25q@host?nodePreference=follower&tlsVerifyCert=false"), 19 | Arguments.of("kurrent://host1,host2:200:300?tlsVerifyCert=false"), 20 | Arguments.of("kurrent://localhost/&tlsVerifyCert=false"), 21 | Arguments.of("kurrent://localhost?tlsVerifyCert=false?nodePreference=follower"), 22 | Arguments.of("kurrent://localhost?tlsVerifyCert=false&nodePreference=any"), 23 | Arguments.of("kurrent://localhost?tlsVerifyCert=if you feel like it"), 24 | Arguments.of("kurrent://localhost?keepAliveInterval=-3"), 25 | Arguments.of("kurrent://localhost?keepAliveInterval=sdfksjsfl"), 26 | Arguments.of("kurrent://localhost?keepAliveTimeout=sdfksjsfl"), 27 | Arguments.of("kurrent://localhost?keepAliveTimeout=-3"), 28 | Arguments.of("kurrent://localhost?nodePreference=read_only_replica"), 29 | Arguments.of("kurrent://localhost?userCertFile=/path/to/cert"), 30 | Arguments.of("kurrent://localhost?userKeyFile=/path/to/key") 31 | ); 32 | } 33 | 34 | @ParameterizedTest 35 | @MethodSource("invalidConnectionStrings") 36 | public void test(String input) throws ConnectionStringParsingException { 37 | Assertions.assertThrows(RuntimeException.class, () -> { 38 | KurrentDBConnectionString.parseOrThrow(input); 39 | }); 40 | } 41 | } 42 | --------------------------------------------------------------------------------