Repository navigation
feat!: Use the v2 API of EventSourcingDB whenever the server offers it, and follow the v2 API in the API of the SDK. - #22
Merged
Merged
Conversation
… its code, description, details, status, and retryAfter. DbApiException now carries the code of the error, the URL that the server sends, such as https://docs.eventsourcingdb.io/errors/precondition-violated, along with its description, its details, its HTTP status, and, for a 429, how long the server asks to wait. ErrorCode holds a constant for every code the server lists, and a test checks them against a copy of that list. The v1 API reports every error with a status code and a text. For the texts of EventSourcingDB 1.2.0 that clearly match an error of the v2 API, the client sets the code of the v2 API, e.g. precondition-violated for a write that is refused with "state conflict: precondition failed". An error that the server reports within a stream is a DbApiException with status 200 now, instead of a plain EventSourcingDbException. BREAKING CHANGE: DbApiException.statusCode() is now status(), and reason() is now description(). Errors within a stream are DbApiExceptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…ve HeartbeatTimeoutException. A stream that receives nothing for the stall timeout, not even a heartbeat, now ends with a ConnectionStalledException, like every other request that stalls, also when observing events or running an EventQL query. The tests that kept a stream alive with heartbeats now cover every stream. BREAKING CHANGE: HeartbeatTimeoutException is removed. Catch ConnectionStalledException instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…OutcomeUnknownException. The server never stops a write halfway, so a write whose answer is lost may have been stored all the same. Writing events and registering an event schema now end with an OutcomeUnknownException, which wraps the failure, whenever that is the case: after a stall, a broken connection, an abort after the request was sent, a 500, a 502 or a 504, or an answer that is not from an EventSourcingDB. A write is reported as certainly not stored only if the client could not connect at all, or if the server refused it, which the v1 API does with a 4xx, a 503, or a 507. BREAKING CHANGE: A write that is interrupted after it was sent, or that fails in another way that leaves its outcome open, now throws an OutcomeUnknownException instead of the failure itself, which is its cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…vents with hasEvents. EventType follows the v2 API now: its name is type, and hasEvents, the opposite of isPhantom, tells whether events of the type were written. The v1 API still sends isPhantom, which the client turns around. BREAKING CHANGE: EventType.eventType() is now type(), and isPhantom() is replaced by hasEvents(), which is its opposite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…PI does. readSubjects takes the subject and ReadSubjectsOptions, whose recursive tells whether to read the subject alone or the subject with every subject nested in it. The v1 API always reads recursively, so the client keeps the subject alone from what it reads, if recursive is false. BREAKING CHANGE: readSubjects(baseSubject) is now readSubjects(subject, options). Pass new ReadSubjectsOptions(true) for what readSubjects did before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…herwise. The first request of a client asks the server whether it offers the v2 API, with GET /api/v2/system/ping. An answer with the ping event means v2, a 404 or a 405 means v1, as EventSourcingDB 1.2.0 answers every path of the v2 API it does not know. The client keeps the answer. Any other outcome fails the request that asked, each request that waited for the same probe with its own action, and the next request asks again. Creating a client still sends no request. TransportChooser is the one place that chooses, and V1Transport and V2Transport stand behind it, so that removing the v1 API later means removing one transport. Requests holds what both share: sending, the stall timeout, and what closing the client ends. On v2, reads use QUERY, every answer comes in an envelope, writes are answered with a stream that keeps them alive with heartbeats, and a finite stream that ends without its end item fails. A write that the v2 API refuses with an error in its answer, or with 400, 401, 408, 413, 429, or 503, was certainly not stored; store-failed and every other failure leave the outcome open. Observing never ends on its own, so a stream of observed events that the server ends without an error now fails with the same error as a finite stream that ends before it is complete, on both APIs. BREAKING CHANGE: A stream of observed events that the server ends without an error now ends with an EventSourcingDbException instead of ending quietly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…er server answers it. A proxy in front of EventSourcingDB 1.2.0 that passes on the v1 API only answers the probe itself, e.g. with a 404 from nginx. The probe took that for an answer that is not from an EventSourcingDB, so every request failed, although the previous release works behind such a proxy. A 404 or a 405 now means the v1 API, whichever server sends it. That costs no safety, since every request to the v1 API checks the Server header anyway. Any other answer from another server still fails, and is not kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…g. /a/b for /a/b/. The v1 API reads subjects recursively only, so the client keeps the subject itself from what it reads, and compares against the subject as the server names it: a slash, followed by the subject without the slashes around it. The comparison did that already; the tests now cover a nested subject with a slash at its end and the root subject, against a fake server and against both images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…ffers. A database that offers the v2 API still answers the v1 API, so the other tests pass whichever API the client chooses. The new test asks the database on its own whether it answers GET /api/v2/system/ping, and checks that the client chose the v2 API exactly then. It goes by the answer rather than by the tag of the image, since once 1.3 is latest, latest offers the v2 API, too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
The new section lists every change of version 2 that may require changes to the code of an application, each with the code before and after, and points to the sections that describe them in detail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…nsport. Everything that is particular to the v1 API now lives in V1Transport and the classes it alone uses, so that removing the v1 API later means removing those, along with the branch of TransportChooser that chooses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
… or 405 means v1. A proxy that passes on the v1 API only answers the probe with a page of its own, e.g. an HTML page from nginx. The probe looks at the status code before anything else, so it takes such an answer for the v1 API, too, and the test now shows it with a 404 and a 405 that are HTML. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…s writes again. With the system property jdk.httpclient.enableAllMethodRetry, the JDK sends a POST again over a new connection, if the connection it was sent over closed before the answer arrived. If that connection is refused, the client took the write for one that was never sent, although the server may have stored it already. The property is now read for each failed write, and while it is set, a write that could not connect is reported as unknown, too. The README says that the property can store a write twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…nds as unknown. The README and the Javadoc said that an interrupt before the request is sent ends a write with a CancellationException. That holds only before the client starts to send it. Once it has, even while the connection is still being established, the write ends with an OutcomeUnknownException, since the client can not tell how far the request got. The texts now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…ved, but before it had its body. close() released what the stream held only the first time it was called. If it came after the headers of the response had arrived, but before the stream had its body, it found nothing to close, and the stream ended without closing the body later on. With an HTTP client of the caller's own, which the client does not shut down, the connection of an observation then stayed open. close() now releases what the stream holds each time it is called, and the stream calls it again once it notices that it is closed, which it does with its next read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…t.properties, too, and keep a value that was true. The JDK reads jdk.httpclient.enableAllMethodRetry once, with the first request of any HTTP client, as a system property that falls back to conf/net.properties of the JDK. The client read the system property only, with each failed write, so it missed a value in net.properties, and took a write that could not connect for one that was not stored once the property had been cleared after the JDK had read it. WriteRetries now reads the property as the JDK does, and keeps a value that was true once, since the JDK may have read it while it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…e, as start documents.
A container whose database was not reachable through its mapped port,
which Docker Desktop sometimes does, failed to start at once instead of
being replaced. Its wait for pings timed out after ten seconds of
connections that the port proxy of Docker accepts and closes without an
answer ("Unexpected end of file from server"). start replaced a container
only if it was still running after the failure, but Testcontainers stops
a container whose wait fails before it reports the failure, so start
never replaced one. The wait for pings now notes whether the container was
running when it failed, and start goes by that. The tests no longer fail
a container that has started, but its wait, so that the failure takes the
path through Testcontainers that a real one takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…, and keep what net.properties holds before a malformed line. The JDK reads jdk.httpclient.enableAllMethodRetry with the first request of any HTTP client. The client read it only when a write failed, so it missed a value that was cleared before. It now reads it when it is created, too, before it sends a request, and keeps a value that was true. The README says when the client sees the property. The JDK goes on with the entries of a malformed conf/net.properties that it read before the line that is malformed. The client failed on such a file with an IllegalArgumentException instead. It now keeps those entries, too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This release lets the client talk to EventSourcingDB 1.3 through its v2 API, while it keeps working with 1.2.0 through the v1 API, and it follows the v2 API in the public API of the SDK.
GET /api/v2/system/ping: the ping event means v2, a 404 or a 405 means v1, whichever server sends it, and the answer is kept. Any other outcome fails that request, and the next one asks again. Requests made at the same time share one probe, and each fails with its own action if the probe fails.TransportChooseris the one place that chooses, withV1TransportandV2Transportbehind it, so that removing the v1 API later is mechanical. On v2, reads useQUERY(RFC 10008), which the README now asks proxies to let through.DbApiExceptionwithcode,description,details,status, andretryAfterfor a 429.ErrorCodeholds a constant for every code the server lists, checked against a copy of that list. For the texts of 1.2.0 that clearly match an error of the v2 API, the v1 path sets the code of the v2 API.OutcomeUnknownException, wrapping the cause, whenever the write may have been stored, by the same rule as every other SDK.ConnectionStalledException. A finite v2 stream without itsenditem fails, and so does observing that the server ends without an error, on both APIs.The tests run every operation against a fake server for each API, so that both transports are fully covered under either image, and a container test checks that the client chooses the API the database offers.
BREAKING CHANGE:
DbApiException.statusCode()is nowstatus(), andreason()is nowdescription(); a v2 server reports the errors of an accepted request with status 200 and a code, and errors within a stream areDbApiExceptions.HeartbeatTimeoutExceptionis removed in favor ofConnectionStalledException. A write that fails in a way that leaves its outcome open, an interrupt after sending included, throws anOutcomeUnknownExceptioninstead of the failure itself.EventType.eventType()is nowtype(), andisPhantom()is replaced by its opposite,hasEvents().readSubjects(baseSubject)is nowreadSubjects(subject, new ReadSubjectsOptions(recursive)). Observing that the server ends without an error now throws anEventSourcingDbException. A proxy in front of the database has to letQUERYthrough.🤖 Generated with Claude Code
https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH