Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ await client.verifyApiToken();

### Talking to Servers With and Without the v2 API

EventSourcingDB offers its v2 API since version 1.3, while older versions offer only the v1 API. This client SDK works with both. Creating a client does not send a request. Instead, the first request of a client asks the server whether it offers the v2 API, by sending a `GET` request to `/api/v2/system/ping`, and the client uses the v2 API if it does, and the v1 API otherwise. The client keeps the answer for its lifetime, so every later request goes straight to the API it found. If the client cannot find out, for example because the server is not reachable, the first request fails with that error, and the next request asks again.
EventSourcingDB offers its v2 API since version 1.3, while older versions offer only the v1 API. This client SDK works with both. Creating a client does not send a request. Instead, the first request of a client asks the server whether it offers the v2 API, by sending a `GET` request to `/api/v2/system/ping`, and the client uses the v2 API if it does, and the v1 API otherwise. The client keeps the answer, so every later request goes straight to the API it found. If the server answers a path of the v2 API with 404 or 405, for example after it was rolled back to a version without it, the client asks again on the next request. If the client cannot find out, for example because the server is not reachable, the first request fails with that error, and the next request asks again.

Through the v2 API, all functions that read use the HTTP method `QUERY` ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)), which, unlike `GET`, carries a request body. If there is a proxy or a load balancer in front of EventSourcingDB, it has to let `QUERY` requests through. There is no fallback to `POST`.

Expand Down
27 changes: 24 additions & 3 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class Client {
// a client finds out which API the server offers, and the client keeps the
// answer. Requests that start meanwhile wait for the same answer. If the
// client fails to find out, the request fails, and the next one tries again.
// The same holds once the client forgets the answer, as #createV2Transport
// describes.
async #getTransport(signal?: AbortSignal): Promise<Transport> {
if (this.#transport !== undefined) {
return this.#transport;
Expand All @@ -68,9 +70,7 @@ class Client {
if (this.#detection === undefined) {
const detection = detectApiVersion(this.#transportOptions).then(apiVersion => {
this.#transport =
apiVersion === 'v2'
? new V2Transport(this.#transportOptions)
: new V1Transport(this.#transportOptions);
apiVersion === 'v2' ? this.#createV2Transport() : new V1Transport(this.#transportOptions);

return this.#transport;
});
Expand All @@ -88,6 +88,27 @@ class Client {
return await waitUnlessAborted(this.#detection, signal);
}

// A server that answers a path of the v2 API with 404 or 405 no longer
// offers it, for example because it was rolled back to a version without
// it, so the client forgets the answer, and the next request finds out
// again, while the request itself fails as before. Only the transport that
// the client still keeps makes it forget. So a late answer to a request
// through a transport that the client has forgotten already neither
// disturbs finding out anew, which may run meanwhile, nor discards what
// that finds.
#createV2Transport(): V2Transport {
const transport = new V2Transport({
...this.#transportOptions,
forgetApiVersion: (): void => {
if (this.#transport === transport) {
this.#transport = undefined;
}
},
});

return transport;
}

public async ping(signal?: AbortSignal): Promise<void> {
const transport = await this.#getTransport(signal);
await transport.ping(signal);
Expand Down
6 changes: 2 additions & 4 deletions src/detectApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readResponseText } from './stall/readResponseText.js';
import { runWithStallGuard } from './stall/runWithStallGuard.js';
import { hasShapeOf } from './types/hasShapeOf.js';
import { isString } from './types/isString.js';
import { isV2NotOffered } from './v2/isV2NotOffered.js';

type ApiVersion = 'v1' | 'v2';

Expand All @@ -23,9 +24,6 @@ const parseJson = (text: string): unknown => {
}
};

const statusNotFound = 404;
const statusMethodNotAllowed = 405;

// Finds out whether the server offers the v2 API, by pinging it there. A
// server that offers only the v1 API, as EventSourcingDB 1.2.0 does, answers
// a path of the v2 API with the status 404 or 405. Every other answer than
Expand All @@ -42,7 +40,7 @@ const detectApiVersion = async ({
method: 'GET',
});

if (response.status === statusNotFound || response.status === statusMethodNotAllowed) {
if (isV2NotOffered(response)) {
await readResponseText(response, stallGuard);
return 'v1';
}
Expand Down
Loading
Loading