eventsourcingdb 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +265 -20
  2. package/dist/Client.d.ts +9 -7
  3. package/dist/Client.js +89 -464
  4. package/dist/ClientOptions.d.ts +4 -0
  5. package/dist/ConnectionStalledError.d.ts +4 -0
  6. package/dist/ConnectionStalledError.js +13 -0
  7. package/dist/DbApiError.d.ts +17 -0
  8. package/dist/DbApiError.js +24 -0
  9. package/dist/ErrorCode.d.ts +68 -0
  10. package/dist/ErrorCode.js +70 -0
  11. package/dist/Event.d.ts +2 -1
  12. package/dist/Event.js +58 -11
  13. package/dist/EventType.d.ts +2 -2
  14. package/dist/HashVerificationFailedError.d.ts +4 -0
  15. package/dist/HashVerificationFailedError.js +7 -0
  16. package/dist/OutcomeUnknownError.d.ts +6 -0
  17. package/dist/OutcomeUnknownError.js +9 -0
  18. package/dist/ReadSubjectsOptions.d.ts +4 -0
  19. package/dist/SignatureVerificationFailedError.d.ts +4 -0
  20. package/dist/SignatureVerificationFailedError.js +7 -0
  21. package/dist/Transport.d.ts +25 -0
  22. package/dist/UnsupportedHashVersionError.d.ts +4 -0
  23. package/dist/UnsupportedHashVersionError.js +7 -0
  24. package/dist/convertCloudEventToEvent.d.ts +1 -1
  25. package/dist/convertCloudEventToEvent.js +6 -1
  26. package/dist/detectApiVersion.d.ts +7 -0
  27. package/dist/detectApiVersion.js +47 -0
  28. package/dist/getImageTag.d.ts +2 -0
  29. package/dist/getImageTag.js +13 -0
  30. package/dist/http/assertEventSourcingDb.d.ts +2 -0
  31. package/dist/http/assertEventSourcingDb.js +14 -0
  32. package/dist/http/createDbApiErrorFromResponse.d.ts +4 -0
  33. package/dist/http/createDbApiErrorFromResponse.js +20 -0
  34. package/dist/http/createStreamEndedError.d.ts +2 -0
  35. package/dist/http/createStreamEndedError.js +6 -0
  36. package/dist/http/isAbortError.d.ts +2 -0
  37. package/dist/http/isAbortError.js +2 -0
  38. package/dist/http/parseRetryAfter.d.ts +2 -0
  39. package/dist/http/parseRetryAfter.js +14 -0
  40. package/dist/http/runWrite.d.ts +6 -0
  41. package/dist/http/runWrite.js +25 -0
  42. package/dist/http/trackSending.d.ts +7 -0
  43. package/dist/http/trackSending.js +46 -0
  44. package/dist/http/waitUnlessAborted.d.ts +2 -0
  45. package/dist/http/waitUnlessAborted.js +21 -0
  46. package/dist/index.d.ts +12 -3
  47. package/dist/index.js +8 -2
  48. package/dist/json/getRawJsonElements.d.ts +2 -0
  49. package/dist/json/getRawJsonElements.js +22 -0
  50. package/dist/json/getRawJsonMember.d.ts +2 -0
  51. package/dist/json/getRawJsonMember.js +29 -0
  52. package/dist/json/scanJson.d.ts +4 -0
  53. package/dist/json/scanJson.js +65 -0
  54. package/dist/ndjson/readNdJsonStream.d.ts +7 -1
  55. package/dist/ndjson/readNdJsonStream.js +7 -28
  56. package/dist/stall/StallGuard.d.ts +13 -0
  57. package/dist/stall/StallGuard.js +77 -0
  58. package/dist/stall/createFetchTimeoutError.d.ts +2 -0
  59. package/dist/stall/createFetchTimeoutError.js +16 -0
  60. package/dist/stall/defaultStallTimeout.d.ts +3 -0
  61. package/dist/stall/defaultStallTimeout.js +6 -0
  62. package/dist/stall/formatDuration.d.ts +2 -0
  63. package/dist/stall/formatDuration.js +9 -0
  64. package/dist/stall/readResponseText.d.ts +3 -0
  65. package/dist/stall/readResponseText.js +26 -0
  66. package/dist/stall/runWithStallGuard.d.ts +6 -0
  67. package/dist/stall/runWithStallGuard.js +18 -0
  68. package/dist/stall/streamWithStallGuard.d.ts +6 -0
  69. package/dist/stall/streamWithStallGuard.js +25 -0
  70. package/dist/v1/V1Transport.d.ts +23 -0
  71. package/dist/v1/V1Transport.js +339 -0
  72. package/dist/v1/getV1ErrorCode.d.ts +4 -0
  73. package/dist/v1/getV1ErrorCode.js +65 -0
  74. package/dist/v1/isV1Refusal.d.ts +3 -0
  75. package/dist/v1/isV1Refusal.js +8 -0
  76. package/dist/v1/stream/StreamHeartbeat.js +1 -0
  77. package/dist/v1/stream/StreamRow.js +1 -0
  78. package/dist/v1/stream/StreamSubject.js +1 -0
  79. package/dist/{stream → v1/stream}/isStreamCloudEvent.js +5 -5
  80. package/dist/{stream → v1/stream}/isStreamError.js +2 -2
  81. package/dist/{stream → v1/stream}/isStreamEventType.js +3 -3
  82. package/dist/{stream → v1/stream}/isStreamHeartbeat.js +1 -1
  83. package/dist/{stream → v1/stream}/isStreamRow.js +1 -1
  84. package/dist/{stream → v1/stream}/isStreamSubject.js +2 -2
  85. package/dist/v2/V2Event.d.ts +16 -0
  86. package/dist/v2/V2Event.js +1 -0
  87. package/dist/v2/V2Item.d.ts +6 -0
  88. package/dist/v2/V2Item.js +1 -0
  89. package/dist/v2/V2Transport.d.ts +23 -0
  90. package/dist/v2/V2Transport.js +302 -0
  91. package/dist/v2/convertV2Item.d.ts +6 -0
  92. package/dist/v2/convertV2Item.js +52 -0
  93. package/dist/v2/isV2Refusal.d.ts +3 -0
  94. package/dist/v2/isV2Refusal.js +12 -0
  95. package/dist/v2/parseV2Item.d.ts +3 -0
  96. package/dist/v2/parseV2Item.js +30 -0
  97. package/dist/v2/readV2Answer.d.ts +4 -0
  98. package/dist/v2/readV2Answer.js +14 -0
  99. package/dist/v2/readV2Stream.d.ts +8 -0
  100. package/dist/v2/readV2Stream.js +34 -0
  101. package/dist/v2/toV2RequestBody.d.ts +8 -0
  102. package/dist/v2/toV2RequestBody.js +46 -0
  103. package/package.json +3 -3
  104. package/dist/HeartbeatTimeoutError.d.ts +0 -4
  105. package/dist/HeartbeatTimeoutError.js +0 -7
  106. package/dist/heartbeatTimeout.d.ts +0 -4
  107. package/dist/heartbeatTimeout.js +0 -6
  108. /package/dist/{stream/StreamCloudEvent.js → ClientOptions.js} +0 -0
  109. /package/dist/{stream/StreamError.js → ReadSubjectsOptions.js} +0 -0
  110. /package/dist/{stream/StreamEventType.js → Transport.js} +0 -0
  111. /package/dist/{stream → v1/stream}/StreamCloudEvent.d.ts +0 -0
  112. /package/dist/{stream/StreamHeartbeat.js → v1/stream/StreamCloudEvent.js} +0 -0
  113. /package/dist/{stream → v1/stream}/StreamError.d.ts +0 -0
  114. /package/dist/{stream/StreamRow.js → v1/stream/StreamError.js} +0 -0
  115. /package/dist/{stream → v1/stream}/StreamEventType.d.ts +0 -0
  116. /package/dist/{stream/StreamSubject.js → v1/stream/StreamEventType.js} +0 -0
  117. /package/dist/{stream → v1/stream}/StreamHeartbeat.d.ts +0 -0
  118. /package/dist/{stream → v1/stream}/StreamRow.d.ts +0 -0
  119. /package/dist/{stream → v1/stream}/StreamSubject.d.ts +0 -0
  120. /package/dist/{stream → v1/stream}/isStreamCloudEvent.d.ts +0 -0
  121. /package/dist/{stream → v1/stream}/isStreamError.d.ts +0 -0
  122. /package/dist/{stream → v1/stream}/isStreamEventType.d.ts +0 -0
  123. /package/dist/{stream → v1/stream}/isStreamHeartbeat.d.ts +0 -0
  124. /package/dist/{stream → v1/stream}/isStreamRow.d.ts +0 -0
  125. /package/dist/{stream → v1/stream}/isStreamSubject.d.ts +0 -0
package/README.md CHANGED
@@ -8,6 +8,8 @@ For more information on EventSourcingDB, see its [official documentation](https:
8
8
 
9
9
  This client SDK includes support for [Testcontainers](https://testcontainers.com/) to spin up EventSourcingDB instances in integration tests. For details, see [Using Testcontainers](#using-testcontainers).
10
10
 
11
+ If you are upgrading from version 1 of this client SDK, see [Upgrading From Version 1](#upgrading-from-version-1).
12
+
11
13
  ## Getting Started
12
14
 
13
15
  Install the client SDK:
@@ -41,6 +43,119 @@ If you want to verify the API token, call `verifyApiToken`. If the token is inva
41
43
  await client.verifyApiToken();
42
44
  ```
43
45
 
46
+ ### Choosing the API
47
+
48
+ 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.
49
+
50
+ 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`.
51
+
52
+ ### Handling Stalled Connections
53
+
54
+ Every request fails once nothing arrives from the server for 30 seconds, for example because the server does not respond, or because a proxy keeps the connection open but no longer passes anything through. The time counts from sending the request until it ends, including connecting and waiting for the response. Everything that arrives starts it anew, so a long stream that keeps receiving events, rows, or heartbeats is never cut off, and the time you spend handling an item of a stream does not count.
55
+
56
+ A request that stalls throws a `ConnectionStalledError`. It extends `TypeError`, since `fetch` throws a `TypeError` when its own timeouts run out, and its `cause` has the `code` that the cause of that `TypeError` has, `UND_ERR_HEADERS_TIMEOUT` while waiting for the response headers and `UND_ERR_BODY_TIMEOUT` afterwards:
57
+
58
+ ```typescript
59
+ import { ConnectionStalledError } from 'eventsourcingdb';
60
+
61
+ try {
62
+ await client.ping();
63
+ } catch (error) {
64
+ if (error instanceof ConnectionStalledError) {
65
+ // ...
66
+ }
67
+ }
68
+ ```
69
+
70
+ To change the 30 seconds, hand over the `stallTimeout` option in milliseconds as third argument when creating the client:
71
+
72
+ ```typescript
73
+ const client = new Client(url, apiToken, {
74
+ stallTimeout: 10_000
75
+ });
76
+ ```
77
+
78
+ *Note that the `fetch` built into Node.js has timeouts of its own, which keep applying: 10 seconds for connecting, 300 seconds for waiting for the response headers, and 300 seconds without data in the response body. If one of them runs out first, the request throws the `TypeError` of `fetch`, with the message `fetch failed` or `terminated`. A `stallTimeout` above these values therefore cannot extend them.*
79
+
80
+ *Note that a write that stalls, a call of `writeEvents` or `registerEventSchema`, throws an `OutcomeUnknownError` whose `cause` is the `ConnectionStalledError`, since the write may have been stored nonetheless, see [Knowing Whether a Write Was Stored](#knowing-whether-a-write-was-stored).*
81
+
82
+ ### Aborting a Request
83
+
84
+ Every function that sends a request takes an abort signal as last argument. To abort a call of `ping`, `verifyApiToken`, or `readEventType`, hand over an abort signal, and abort the appropriate `AbortController`. The call then throws the reason of the abort, which is an `AbortError` unless you provide a reason of your own:
85
+
86
+ ```typescript
87
+ const controller = new AbortController();
88
+
89
+ try {
90
+ await client.readEventType('io.eventsourcingdb.library.book-acquired', controller.signal);
91
+ } catch (error) {
92
+ if (error instanceof DOMException && error.name === 'AbortError') {
93
+ // ...
94
+ }
95
+ }
96
+
97
+ // Somewhere else, abort the controller, which will cause
98
+ // readEventType to throw.
99
+ controller.abort();
100
+ ```
101
+
102
+ Aborting `writeEvents` or `registerEventSchema` once the write has been sent does not undo it, so the write may have been stored nonetheless. The call then throws an `OutcomeUnknownError` whose `cause` is the reason of the abort, see [Knowing Whether a Write Was Stored](#knowing-whether-a-write-was-stored). If the write has not been sent yet, because the signal was aborted before or while the client SDK still establishes the connection, the call throws the reason of the abort itself.
103
+
104
+ *Note that aborting a function that returns an asynchronous iterator, such as `readEvents`, does not throw, but ends the iteration, see [Aborting Reading](#aborting-reading).*
105
+
106
+ ### Handling Errors
107
+
108
+ Every error that the server reports throws a `DbApiError`, whether the server describes it in its answer, answers with an HTTP status other than `200`, or reports it while a stream is running. A `DbApiError` has the following fields:
109
+
110
+ - `code` is the URL that names the error, exactly as the server sends it, for example `https://docs.eventsourcingdb.io/errors/precondition-violated`. It doubles as a link to the documentation of the error. It is `undefined` for an error without a code.
111
+ - `description` is the text that describes the error.
112
+ - `details` is an object with details on the error, such as the index of the precondition that does not hold. It is empty for an error without details.
113
+ - `status` is the HTTP status of the answer, which is `200` for an error that the server describes in its answer.
114
+ - `retryAfter` is the number of seconds after which the server accepts requests again, which only an answer with the HTTP status `429` may name. The client SDK never repeats a request on its own.
115
+
116
+ To tell errors apart, compare the `code` with the constants of `ErrorCode`, which has a constant for each code that the server knows:
117
+
118
+ ```typescript
119
+ import { DbApiError, ErrorCode, isSubjectPristine } from 'eventsourcingdb';
120
+
121
+ try {
122
+ await client.writeEvents([
123
+ // events
124
+ ], [
125
+ isSubjectPristine('/books/42')
126
+ ]);
127
+ } catch (error) {
128
+ if (error instanceof DbApiError && error.code === ErrorCode.preconditionViolated) {
129
+ // ...
130
+ }
131
+ }
132
+ ```
133
+
134
+ The v1 API does not send codes. For the errors of the v1 API that clearly match a code of the v2 API, the client SDK sets that code, so that the same code works with both APIs: `preconditionViolated`, `capacityExceeded`, `eventSchemaViolated`, and `licenseInvalid` for writing events, `eventSchemaAlreadyRegistered` and `eventSchemaViolated` for registering an event schema, `rangeUnsatisfiable` for reading events, `eventTypeUnknown` for reading an event type, and `storeReadOnly` for every request. Every other error of the v1 API has no code.
135
+
136
+ A stream that the server ends with an end item, which every stream of the v2 API except observing does, throws an error if it breaks off before that item. Observing never ends on its own, so a stream of observed events that the server closes without reporting an error throws the same error, with both APIs.
137
+
138
+ #### Knowing Whether a Write Was Stored
139
+
140
+ EventSourcingDB never stops a write halfway, so a write whose answer is lost may have been stored nonetheless. If `writeEvents` or `registerEventSchema` fails once the write may have reached the server, it throws an `OutcomeUnknownError`, whose `cause` tells what went wrong, for example a `ConnectionStalledError`, a broken connection, an abort after sending, a stream that broke off, or an answer with the HTTP status `500`:
141
+
142
+ ```typescript
143
+ import { OutcomeUnknownError } from 'eventsourcingdb';
144
+
145
+ try {
146
+ await client.writeEvents([
147
+ // events
148
+ ]);
149
+ } catch (error) {
150
+ if (error instanceof OutcomeUnknownError) {
151
+ // Find out whether the events were stored, for example by
152
+ // reading them, before writing them again.
153
+ }
154
+ }
155
+ ```
156
+
157
+ A write certainly was not stored, and throws the error itself, only if its request never went out, for example since the client SDK could not establish a connection, or since the write was aborted or stalled while connecting, or if the server refused the write before storing anything. A request counts as gone out once its headers have been sent. The server refuses a write with every `DbApiError` except one with the code `ErrorCode.storeFailed`, and, through the v2 API, with the HTTP status `400`, `401`, `408`, `413`, `429`, or `503`, or, through the v1 API, with an HTTP status of `4xx`, `503`, or `507`. A write that the server refuses with the HTTP status `429` may be sent again after `retryAfter` seconds.
158
+
44
159
  ### Writing Events
45
160
 
46
161
  Call the `writeEvents` function and hand over an array with one or more events. You do not have to provide all event fields – some are automatically added by the server.
@@ -262,10 +377,10 @@ controller.abort();
262
377
 
263
378
  #### Detecting a Stalled Connection
264
379
 
265
- While a query is running, EventSourcingDB sends a heartbeat every second whenever there is no row to send. If neither a row nor a heartbeat arrives for 30 seconds, for example because a proxy keeps the connection open but no longer passes anything through, the client SDK closes the connection and the iterator throws a `HeartbeatTimeoutError`:
380
+ While a query is running, EventSourcingDB sends a heartbeat every second whenever there is no row to send. If neither a row nor a heartbeat arrives for the stall timeout, 30 seconds by default, for example because a proxy keeps the connection open but no longer passes anything through, the client SDK closes the connection and the iterator throws a `ConnectionStalledError`, see [Handling Stalled Connections](#handling-stalled-connections):
266
381
 
267
382
  ```typescript
268
- import { HeartbeatTimeoutError } from 'eventsourcingdb';
383
+ import { ConnectionStalledError } from 'eventsourcingdb';
269
384
 
270
385
  try {
271
386
  for await (const row of client.runEventQlQuery(`
@@ -275,13 +390,13 @@ try {
275
390
  // ...
276
391
  }
277
392
  } catch (error) {
278
- if (error instanceof HeartbeatTimeoutError) {
393
+ if (error instanceof ConnectionStalledError) {
279
394
  // ...
280
395
  }
281
396
  }
282
397
  ```
283
398
 
284
- *Note that the time you spend handling a row does not count towards the 30 seconds, and that aborting the query does not cause a `HeartbeatTimeoutError`.*
399
+ *Note that the time you spend handling a row does not count towards the stall timeout, and that aborting the query does not cause a `ConnectionStalledError`.*
285
400
 
286
401
  ### Observing Events
287
402
 
@@ -297,6 +412,8 @@ for await (const event of client.observeEvents('/books/42', {
297
412
  }
298
413
  ```
299
414
 
415
+ *Note that observing never ends on its own. If the server closes the stream without reporting an error, the iterator throws an error, so that a failure does not go unnoticed. To stop observing, use `break`, `return`, or an abort signal, see [Aborting Observing](#aborting-observing).*
416
+
300
417
  #### Observing From Subjects Recursively
301
418
 
302
419
  If you want to observe not only all the events of a subject, but also the events of all nested subjects, set the `recursive` option to `true`:
@@ -369,10 +486,10 @@ controller.abort();
369
486
 
370
487
  #### Detecting a Stalled Connection
371
488
 
372
- While observing, EventSourcingDB sends a heartbeat every second whenever there is no event to send. If neither an event nor a heartbeat arrives for 30 seconds, for example because a proxy keeps the connection open but no longer passes anything through, the client SDK closes the connection and the iterator throws a `HeartbeatTimeoutError`:
489
+ While observing, EventSourcingDB sends a heartbeat every second whenever there is no event to send. If neither an event nor a heartbeat arrives for the stall timeout, 30 seconds by default, for example because a proxy keeps the connection open but no longer passes anything through, the client SDK closes the connection and the iterator throws a `ConnectionStalledError`, see [Handling Stalled Connections](#handling-stalled-connections):
373
490
 
374
491
  ```typescript
375
- import { HeartbeatTimeoutError } from 'eventsourcingdb';
492
+ import { ConnectionStalledError } from 'eventsourcingdb';
376
493
 
377
494
  try {
378
495
  for await (const event of client.observeEvents('/books/42', {
@@ -381,13 +498,13 @@ try {
381
498
  // ...
382
499
  }
383
500
  } catch (error) {
384
- if (error instanceof HeartbeatTimeoutError) {
501
+ if (error instanceof ConnectionStalledError) {
385
502
  // ...
386
503
  }
387
504
  }
388
505
  ```
389
506
 
390
- *Note that the time you spend handling an event does not count towards the 30 seconds, and that aborting observing does not cause a `HeartbeatTimeoutError`.*
507
+ *Note that the time you spend handling an event does not count towards the stall timeout, and that aborting observing does not cause a `ConnectionStalledError`.*
391
508
 
392
509
  ### Registering an Event Schema
393
510
 
@@ -410,20 +527,36 @@ await client.registerEventSchema('io.eventsourcingdb.library.book-acquired', {
410
527
  });
411
528
  ```
412
529
 
530
+ If a schema is already registered for the event type, the function throws a `DbApiError` with the code `ErrorCode.eventSchemaAlreadyRegistered`, since a schema can be neither replaced nor removed.
531
+
413
532
  ### Listing Subjects
414
533
 
415
- To list all subjects, call the `readSubjects` function with `/` as the base subject. The function returns an asynchronous iterator, which you can use e.g. inside a `for await` loop:
534
+ To list subjects, call the `readSubjects` function with a subject as the first argument and an options object as the second argument. The `recursive` option is required. Set it to `true` to list the subject together with all its nested subjects, so that `/` lists all subjects. The function returns an asynchronous iterator, which you can use e.g. inside a `for await` loop:
416
535
 
417
536
  ```typescript
418
- for await (const subject of client.readSubjects('/')) {
537
+ for await (const subject of client.readSubjects('/', {
538
+ recursive: true
539
+ })) {
540
+ // ...
541
+ }
542
+ ```
543
+
544
+ If you only want to list subjects within a specific branch, provide the desired subject instead:
545
+
546
+ ```typescript
547
+ for await (const subject of client.readSubjects('/books', {
548
+ recursive: true
549
+ })) {
419
550
  // ...
420
551
  }
421
552
  ```
422
553
 
423
- If you only want to list subjects within a specific branch, provide the desired base subject instead:
554
+ To find out whether a subject exists, set the `recursive` option to `false`. The iterator then returns only the subject itself, if it exists:
424
555
 
425
556
  ```typescript
426
- for await (const subject of client.readSubjects('/books')) {
557
+ for await (const subject of client.readSubjects('/books/42', {
558
+ recursive: false
559
+ })) {
427
560
  // ...
428
561
  }
429
562
  ```
@@ -432,14 +565,14 @@ for await (const subject of client.readSubjects('/books')) {
432
565
 
433
566
  If you need to abort listing use `break` or `return` within the `for await` loop. However, this only works if there is currently an iteration going on.
434
567
 
435
- To abort listing independently of that, hand over an abort signal as second argument when calling `readSubjects`, and abort the appropriate `AbortController`:
568
+ To abort listing independently of that, hand over an abort signal as third argument when calling `readSubjects`, and abort the appropriate `AbortController`:
436
569
 
437
570
  ```typescript
438
571
  const controller = new AbortController();
439
572
 
440
- for await (const subject of client.readSubjects(
441
- '/', controller.signal)
442
- ) {
573
+ for await (const subject of client.readSubjects('/', {
574
+ recursive: true
575
+ }, controller.signal)) {
443
576
  // ...
444
577
  }
445
578
 
@@ -458,6 +591,8 @@ for await (const eventType of client.readEventTypes()) {
458
591
  }
459
592
  ```
460
593
 
594
+ Each event type has the fields `type`, which names it, `hasEvents`, which tells whether there are events of this type, and `schema`, which is the registered schema, if there is one.
595
+
461
596
  #### Aborting Listing
462
597
 
463
598
  If you need to abort listing use `break` or `return` within the `for await` loop. However, this only works if there is currently an iteration going on.
@@ -467,7 +602,7 @@ To abort listing independently of that, hand over an abort signal as argument wh
467
602
  ```typescript
468
603
  const controller = new AbortController();
469
604
 
470
- for await (const eventType of client.readEventTypes()) {
605
+ for await (const eventType of client.readEventTypes(controller.signal)) {
471
606
  // ...
472
607
  }
473
608
 
@@ -481,24 +616,43 @@ controller.abort();
481
616
  To list a specific event type, call the `readEventType` function with the event type as an argument. The function returns the detailed event type, which includes the schema:
482
617
 
483
618
  ```typescript
484
- eventType = await client.readEventType("io.eventsourcingdb.library.book-acquired");
619
+ const eventType = await client.readEventType('io.eventsourcingdb.library.book-acquired');
485
620
  ```
486
621
 
622
+ If there are neither events of this type nor a registered schema, the function throws a `DbApiError` with the code `ErrorCode.eventTypeUnknown`.
623
+
487
624
  ### Verifying an Event's Hash
488
625
 
489
- To verify the integrity of an event, call the `verifyHash` function on the event instance. This recomputes the event's hash locally and compares it to the hash stored in the event. If the hashes differ, the function returns an error:
626
+ To verify the integrity of an event, call the `verifyHash` function on the event instance. This recomputes the event's hash locally and compares it to the hash stored in the event. If the hashes differ, or if the event's `data` has been changed since it was received, the function throws a `HashVerificationFailedError`:
490
627
 
491
628
  ```typescript
492
629
  event.verifyHash();
493
630
  ```
494
631
 
632
+ The hash of an event tells its version. A hash that starts with `esdb:hash:v2:` is of version 2, which EventSourcingDB writes since version 1.3. A hash without a prefix is of version 1, which events written before keep. `verifyHash` supports both. If the hash is of a version the client SDK does not know, the function throws an `UnsupportedHashVersionError` that names the version, since such a hash cannot be verified, rather than being invalid:
633
+
634
+ ```typescript
635
+ import { HashVerificationFailedError, UnsupportedHashVersionError } from 'eventsourcingdb';
636
+
637
+ try {
638
+ event.verifyHash();
639
+ } catch (error) {
640
+ if (error instanceof HashVerificationFailedError) {
641
+ // ...
642
+ }
643
+ if (error instanceof UnsupportedHashVersionError) {
644
+ // ...
645
+ }
646
+ }
647
+ ```
648
+
495
649
  *Note that this only verifies the hash. If you also want to verify the signature, you can skip this step and call `verifySignature` directly, which performs a hash verification internally.*
496
650
 
497
651
  ### Verifying an Event's Signature
498
652
 
499
653
  To verify the authenticity of an event, call the `verifySignature` function on the event instance. This requires the public key that matches the private key used for signing on the server.
500
654
 
501
- The function first verifies the event's hash, and then checks the signature. If any verification step fails, it returns an error:
655
+ The function first verifies the event's hash, and then checks the signature. If any verification step fails, it throws an error, for example a `SignatureVerificationFailedError` if the signature is invalid:
502
656
 
503
657
  ```typescript
504
658
  const verificationKey = // an ed25519 public key
@@ -570,3 +724,94 @@ In case you need to set up the client yourself, use the following functions to g
570
724
  - `getMappedPort()` returns the port
571
725
  - `getBaseUrl()` returns the full URL of the container
572
726
  - `getApiToken()` returns the API token
727
+
728
+ ## Upgrading From Version 1
729
+
730
+ Version 2 of this client SDK speaks the v2 API of EventSourcingDB 1.3 as well as the v1 API of older versions, see [Choosing the API](#choosing-the-api). It brings the following breaking changes:
731
+
732
+ - **Event types:** An event type names its type in the field `type` instead of `eventType`, and tells whether it has events in the field `hasEvents` instead of `isPhantom`, which meant the opposite.
733
+
734
+ ```typescript
735
+ // Before
736
+ if (!eventType.isPhantom) {
737
+ console.log(eventType.eventType);
738
+ }
739
+
740
+ // After
741
+ if (eventType.hasEvents) {
742
+ console.log(eventType.type);
743
+ }
744
+ ```
745
+
746
+ - **Listing subjects:** `readSubjects` takes the subject and an options object with the required `recursive` option, instead of a base subject alone. Calling it without the option throws an error. To keep the previous behavior, set `recursive` to `true`. The abort signal moves from the second to the third argument.
747
+
748
+ ```typescript
749
+ // Before
750
+ client.readSubjects('/books', signal);
751
+
752
+ // After
753
+ client.readSubjects('/books', { recursive: true }, signal);
754
+ ```
755
+
756
+ - **Errors of the server:** Every error that the server reports throws a `DbApiError` with `code`, `description`, `details`, `status`, and `retryAfter`, instead of an `Error` whose message names the HTTP status, see [Handling Errors](#handling-errors).
757
+
758
+ ```typescript
759
+ // Before
760
+ if (error instanceof Error && error.message.includes("'409'")) {
761
+ // ...
762
+ }
763
+
764
+ // After
765
+ if (error instanceof DbApiError && error.code === ErrorCode.preconditionViolated) {
766
+ // ...
767
+ }
768
+ ```
769
+
770
+ - **Stalled streams:** `HeartbeatTimeoutError` is gone. A stream in which neither an item nor a heartbeat arrives in time throws a `ConnectionStalledError`, as every other request that stalls does, whose `cause` has the `code` `UND_ERR_BODY_TIMEOUT`.
771
+
772
+ ```typescript
773
+ // Before
774
+ if (error instanceof HeartbeatTimeoutError) {
775
+ // ...
776
+ }
777
+
778
+ // After
779
+ if (error instanceof ConnectionStalledError) {
780
+ // ...
781
+ }
782
+ ```
783
+
784
+ - **Failed writes:** `writeEvents` and `registerEventSchema` throw an `OutcomeUnknownError`, whose `cause` is the original error, whenever the write may have been stored nonetheless, see [Knowing Whether a Write Was Stored](#knowing-whether-a-write-was-stored). This includes aborting the signal once the write has been sent, which threw the reason of the abort before, and a write that stalls, which threw a `ConnectionStalledError` before.
785
+
786
+ ```typescript
787
+ // Before
788
+ if (error instanceof ConnectionStalledError) {
789
+ // ...
790
+ }
791
+
792
+ // After
793
+ if (error instanceof OutcomeUnknownError && error.cause instanceof ConnectionStalledError) {
794
+ // ...
795
+ }
796
+ ```
797
+
798
+ - **Observing that ends:** A stream of observed events that the server closes without reporting an error throws an error, instead of ending the iteration quietly. Stopping with `break`, `return`, or an abort signal still ends it without an error.
799
+
800
+ ```typescript
801
+ // Before
802
+ for await (const event of client.observeEvents('/books', { recursive: true })) {
803
+ // ...
804
+ }
805
+ // Reached once the server closed the stream.
806
+
807
+ // After
808
+ try {
809
+ for await (const event of client.observeEvents('/books', { recursive: true })) {
810
+ // ...
811
+ }
812
+ } catch (error) {
813
+ // Reached once the server closed the stream.
814
+ }
815
+ ```
816
+
817
+ - **Reading through a proxy:** Against EventSourcingDB 1.3 and later, the functions that read use the HTTP method `QUERY` instead of `POST`, which a proxy in front of the database has to let through, see [Choosing the API](#choosing-the-api).
package/dist/Client.d.ts CHANGED
@@ -1,21 +1,23 @@
1
+ import type { ClientOptions } from './ClientOptions.js';
1
2
  import type { Event } from './Event.js';
2
3
  import type { EventCandidate } from './EventCandidate.js';
3
4
  import type { EventType } from './EventType.js';
4
5
  import type { ObserveEventsOptions } from './ObserveEventsOptions.js';
5
6
  import type { Precondition } from './Precondition.js';
6
7
  import type { ReadEventsOptions } from './ReadEventsOptions.js';
8
+ import type { ReadSubjectsOptions } from './ReadSubjectsOptions.js';
7
9
  declare class Client {
8
10
  #private;
9
- constructor(url: URL, apiToken: string);
10
- ping(): Promise<void>;
11
- verifyApiToken(): Promise<void>;
12
- writeEvents(events: EventCandidate[], preconditions?: Precondition[]): Promise<Event[]>;
11
+ constructor(url: URL, apiToken: string, options?: ClientOptions);
12
+ ping(signal?: AbortSignal): Promise<void>;
13
+ verifyApiToken(signal?: AbortSignal): Promise<void>;
14
+ writeEvents(events: EventCandidate[], preconditions?: Precondition[], signal?: AbortSignal): Promise<Event[]>;
13
15
  readEvents(subject: string, options: ReadEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
14
16
  runEventQlQuery(query: string, signal?: AbortSignal): AsyncGenerator<unknown, void, void>;
15
17
  observeEvents(subject: string, options: ObserveEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
16
- registerEventSchema(eventType: string, schema: Record<string, unknown>): Promise<void>;
17
- readSubjects(baseSubject: string, signal?: AbortSignal): AsyncGenerator<string, void, void>;
18
- readEventType(eventType: string): Promise<EventType>;
18
+ registerEventSchema(eventType: string, schema: Record<string, unknown>, signal?: AbortSignal): Promise<void>;
19
+ readSubjects(subject: string, options: ReadSubjectsOptions, signal?: AbortSignal): AsyncGenerator<string, void, void>;
20
+ readEventType(eventType: string, signal?: AbortSignal): Promise<EventType>;
19
21
  readEventTypes(signal?: AbortSignal): AsyncGenerator<EventType, void, void>;
20
22
  }
21
23
  export { Client };