eventsourcingdb 1.10.0 → 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 (89) hide show
  1. package/README.md +198 -24
  2. package/dist/Client.d.ts +2 -1
  3. package/dist/Client.js +79 -439
  4. package/dist/DbApiError.d.ts +17 -0
  5. package/dist/DbApiError.js +24 -0
  6. package/dist/ErrorCode.d.ts +68 -0
  7. package/dist/ErrorCode.js +70 -0
  8. package/dist/EventType.d.ts +2 -2
  9. package/dist/OutcomeUnknownError.d.ts +6 -0
  10. package/dist/OutcomeUnknownError.js +9 -0
  11. package/dist/ReadSubjectsOptions.d.ts +4 -0
  12. package/dist/Transport.d.ts +25 -0
  13. package/dist/detectApiVersion.d.ts +7 -0
  14. package/dist/detectApiVersion.js +47 -0
  15. package/dist/http/assertEventSourcingDb.d.ts +2 -0
  16. package/dist/http/assertEventSourcingDb.js +14 -0
  17. package/dist/http/createDbApiErrorFromResponse.d.ts +4 -0
  18. package/dist/http/createDbApiErrorFromResponse.js +20 -0
  19. package/dist/http/createStreamEndedError.d.ts +2 -0
  20. package/dist/http/createStreamEndedError.js +6 -0
  21. package/dist/http/isAbortError.d.ts +2 -0
  22. package/dist/http/isAbortError.js +2 -0
  23. package/dist/http/parseRetryAfter.d.ts +2 -0
  24. package/dist/http/parseRetryAfter.js +14 -0
  25. package/dist/http/runWrite.d.ts +6 -0
  26. package/dist/http/runWrite.js +25 -0
  27. package/dist/http/trackSending.d.ts +7 -0
  28. package/dist/http/trackSending.js +46 -0
  29. package/dist/http/waitUnlessAborted.d.ts +2 -0
  30. package/dist/http/waitUnlessAborted.js +21 -0
  31. package/dist/index.d.ts +7 -3
  32. package/dist/index.js +4 -2
  33. package/dist/stall/StallGuard.d.ts +0 -1
  34. package/dist/stall/StallGuard.js +3 -9
  35. package/dist/stall/streamWithStallGuard.d.ts +6 -0
  36. package/dist/stall/streamWithStallGuard.js +25 -0
  37. package/dist/v1/V1Transport.d.ts +23 -0
  38. package/dist/v1/V1Transport.js +339 -0
  39. package/dist/v1/getV1ErrorCode.d.ts +4 -0
  40. package/dist/v1/getV1ErrorCode.js +65 -0
  41. package/dist/v1/isV1Refusal.d.ts +3 -0
  42. package/dist/v1/isV1Refusal.js +8 -0
  43. package/dist/v1/stream/StreamRow.js +1 -0
  44. package/dist/v1/stream/StreamSubject.js +1 -0
  45. package/dist/{stream → v1/stream}/isStreamCloudEvent.js +5 -5
  46. package/dist/{stream → v1/stream}/isStreamError.js +2 -2
  47. package/dist/{stream → v1/stream}/isStreamEventType.js +3 -3
  48. package/dist/{stream → v1/stream}/isStreamHeartbeat.js +1 -1
  49. package/dist/{stream → v1/stream}/isStreamRow.js +1 -1
  50. package/dist/{stream → v1/stream}/isStreamSubject.js +2 -2
  51. package/dist/v2/V2Event.d.ts +16 -0
  52. package/dist/v2/V2Event.js +1 -0
  53. package/dist/v2/V2Item.d.ts +6 -0
  54. package/dist/v2/V2Item.js +1 -0
  55. package/dist/v2/V2Transport.d.ts +23 -0
  56. package/dist/v2/V2Transport.js +302 -0
  57. package/dist/v2/convertV2Item.d.ts +6 -0
  58. package/dist/v2/convertV2Item.js +52 -0
  59. package/dist/v2/isV2Refusal.d.ts +3 -0
  60. package/dist/v2/isV2Refusal.js +12 -0
  61. package/dist/v2/parseV2Item.d.ts +3 -0
  62. package/dist/v2/parseV2Item.js +30 -0
  63. package/dist/v2/readV2Answer.d.ts +4 -0
  64. package/dist/v2/readV2Answer.js +14 -0
  65. package/dist/v2/readV2Stream.d.ts +8 -0
  66. package/dist/v2/readV2Stream.js +34 -0
  67. package/dist/v2/toV2RequestBody.d.ts +8 -0
  68. package/dist/v2/toV2RequestBody.js +46 -0
  69. package/package.json +1 -1
  70. package/dist/HeartbeatTimeoutError.d.ts +0 -4
  71. package/dist/HeartbeatTimeoutError.js +0 -9
  72. /package/dist/{stream/StreamCloudEvent.js → ReadSubjectsOptions.js} +0 -0
  73. /package/dist/{stream/StreamError.js → Transport.js} +0 -0
  74. /package/dist/{stream → v1/stream}/StreamCloudEvent.d.ts +0 -0
  75. /package/dist/{stream/StreamEventType.js → v1/stream/StreamCloudEvent.js} +0 -0
  76. /package/dist/{stream → v1/stream}/StreamError.d.ts +0 -0
  77. /package/dist/{stream/StreamHeartbeat.js → v1/stream/StreamError.js} +0 -0
  78. /package/dist/{stream → v1/stream}/StreamEventType.d.ts +0 -0
  79. /package/dist/{stream/StreamRow.js → v1/stream/StreamEventType.js} +0 -0
  80. /package/dist/{stream → v1/stream}/StreamHeartbeat.d.ts +0 -0
  81. /package/dist/{stream/StreamSubject.js → v1/stream/StreamHeartbeat.js} +0 -0
  82. /package/dist/{stream → v1/stream}/StreamRow.d.ts +0 -0
  83. /package/dist/{stream → v1/stream}/StreamSubject.d.ts +0 -0
  84. /package/dist/{stream → v1/stream}/isStreamCloudEvent.d.ts +0 -0
  85. /package/dist/{stream → v1/stream}/isStreamError.d.ts +0 -0
  86. /package/dist/{stream → v1/stream}/isStreamEventType.d.ts +0 -0
  87. /package/dist/{stream → v1/stream}/isStreamHeartbeat.d.ts +0 -0
  88. /package/dist/{stream → v1/stream}/isStreamRow.d.ts +0 -0
  89. /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,12 @@ 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
+
44
52
  ### Handling Stalled Connections
45
53
 
46
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.
@@ -69,19 +77,17 @@ const client = new Client(url, apiToken, {
69
77
 
70
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.*
71
79
 
72
- *Note that `runEventQlQuery` and `observeEvents` throw a `HeartbeatTimeoutError` instead once the server has started to respond, see [Running EventQL Queries](#running-eventql-queries) and [Observing Events](#observing-events).*
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).*
73
81
 
74
82
  ### Aborting a Request
75
83
 
76
- To abort a call of `ping`, `verifyApiToken`, `writeEvents`, `registerEventSchema`, or `readEventType`, hand over an abort signal as last argument, 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:
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:
77
85
 
78
86
  ```typescript
79
87
  const controller = new AbortController();
80
88
 
81
89
  try {
82
- await client.writeEvents([
83
- // events
84
- ], [], controller.signal);
90
+ await client.readEventType('io.eventsourcingdb.library.book-acquired', controller.signal);
85
91
  } catch (error) {
86
92
  if (error instanceof DOMException && error.name === 'AbortError') {
87
93
  // ...
@@ -89,12 +95,67 @@ try {
89
95
  }
90
96
 
91
97
  // Somewhere else, abort the controller, which will cause
92
- // writeEvents to throw.
98
+ // readEventType to throw.
93
99
  controller.abort();
94
100
  ```
95
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
+
96
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).*
97
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
+
98
159
  ### Writing Events
99
160
 
100
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.
@@ -316,10 +377,10 @@ controller.abort();
316
377
 
317
378
  #### Detecting a Stalled Connection
318
379
 
319
- 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 `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):
320
381
 
321
382
  ```typescript
322
- import { HeartbeatTimeoutError } from 'eventsourcingdb';
383
+ import { ConnectionStalledError } from 'eventsourcingdb';
323
384
 
324
385
  try {
325
386
  for await (const row of client.runEventQlQuery(`
@@ -329,13 +390,13 @@ try {
329
390
  // ...
330
391
  }
331
392
  } catch (error) {
332
- if (error instanceof HeartbeatTimeoutError) {
393
+ if (error instanceof ConnectionStalledError) {
333
394
  // ...
334
395
  }
335
396
  }
336
397
  ```
337
398
 
338
- *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 `HeartbeatTimeoutError`. If the server does not start to respond in time, the iterator throws a `ConnectionStalledError`, see [Handling Stalled Connections](#handling-stalled-connections).*
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`.*
339
400
 
340
401
  ### Observing Events
341
402
 
@@ -351,6 +412,8 @@ for await (const event of client.observeEvents('/books/42', {
351
412
  }
352
413
  ```
353
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
+
354
417
  #### Observing From Subjects Recursively
355
418
 
356
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`:
@@ -423,10 +486,10 @@ controller.abort();
423
486
 
424
487
  #### Detecting a Stalled Connection
425
488
 
426
- 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 `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):
427
490
 
428
491
  ```typescript
429
- import { HeartbeatTimeoutError } from 'eventsourcingdb';
492
+ import { ConnectionStalledError } from 'eventsourcingdb';
430
493
 
431
494
  try {
432
495
  for await (const event of client.observeEvents('/books/42', {
@@ -435,13 +498,13 @@ try {
435
498
  // ...
436
499
  }
437
500
  } catch (error) {
438
- if (error instanceof HeartbeatTimeoutError) {
501
+ if (error instanceof ConnectionStalledError) {
439
502
  // ...
440
503
  }
441
504
  }
442
505
  ```
443
506
 
444
- *Note that the time you spend handling an event does not count towards the stall timeout, and that aborting observing does not cause a `HeartbeatTimeoutError`. If the server does not start to respond in time, the iterator throws a `ConnectionStalledError`, see [Handling Stalled Connections](#handling-stalled-connections).*
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`.*
445
508
 
446
509
  ### Registering an Event Schema
447
510
 
@@ -464,20 +527,36 @@ await client.registerEventSchema('io.eventsourcingdb.library.book-acquired', {
464
527
  });
465
528
  ```
466
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
+
467
532
  ### Listing Subjects
468
533
 
469
- 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:
535
+
536
+ ```typescript
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:
470
545
 
471
546
  ```typescript
472
- for await (const subject of client.readSubjects('/')) {
547
+ for await (const subject of client.readSubjects('/books', {
548
+ recursive: true
549
+ })) {
473
550
  // ...
474
551
  }
475
552
  ```
476
553
 
477
- 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:
478
555
 
479
556
  ```typescript
480
- for await (const subject of client.readSubjects('/books')) {
557
+ for await (const subject of client.readSubjects('/books/42', {
558
+ recursive: false
559
+ })) {
481
560
  // ...
482
561
  }
483
562
  ```
@@ -486,14 +565,14 @@ for await (const subject of client.readSubjects('/books')) {
486
565
 
487
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.
488
567
 
489
- 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`:
490
569
 
491
570
  ```typescript
492
571
  const controller = new AbortController();
493
572
 
494
- for await (const subject of client.readSubjects(
495
- '/', controller.signal)
496
- ) {
573
+ for await (const subject of client.readSubjects('/', {
574
+ recursive: true
575
+ }, controller.signal)) {
497
576
  // ...
498
577
  }
499
578
 
@@ -512,6 +591,8 @@ for await (const eventType of client.readEventTypes()) {
512
591
  }
513
592
  ```
514
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
+
515
596
  #### Aborting Listing
516
597
 
517
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.
@@ -521,7 +602,7 @@ To abort listing independently of that, hand over an abort signal as argument wh
521
602
  ```typescript
522
603
  const controller = new AbortController();
523
604
 
524
- for await (const eventType of client.readEventTypes()) {
605
+ for await (const eventType of client.readEventTypes(controller.signal)) {
525
606
  // ...
526
607
  }
527
608
 
@@ -535,9 +616,11 @@ controller.abort();
535
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:
536
617
 
537
618
  ```typescript
538
- eventType = await client.readEventType("io.eventsourcingdb.library.book-acquired");
619
+ const eventType = await client.readEventType('io.eventsourcingdb.library.book-acquired');
539
620
  ```
540
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
+
541
624
  ### Verifying an Event's Hash
542
625
 
543
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`:
@@ -641,3 +724,94 @@ In case you need to set up the client yourself, use the following functions to g
641
724
  - `getMappedPort()` returns the port
642
725
  - `getBaseUrl()` returns the full URL of the container
643
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
@@ -5,6 +5,7 @@ import type { EventType } from './EventType.js';
5
5
  import type { ObserveEventsOptions } from './ObserveEventsOptions.js';
6
6
  import type { Precondition } from './Precondition.js';
7
7
  import type { ReadEventsOptions } from './ReadEventsOptions.js';
8
+ import type { ReadSubjectsOptions } from './ReadSubjectsOptions.js';
8
9
  declare class Client {
9
10
  #private;
10
11
  constructor(url: URL, apiToken: string, options?: ClientOptions);
@@ -15,7 +16,7 @@ declare class Client {
15
16
  runEventQlQuery(query: string, signal?: AbortSignal): AsyncGenerator<unknown, void, void>;
16
17
  observeEvents(subject: string, options: ObserveEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
17
18
  registerEventSchema(eventType: string, schema: Record<string, unknown>, signal?: AbortSignal): Promise<void>;
18
- readSubjects(baseSubject: string, signal?: AbortSignal): AsyncGenerator<string, void, void>;
19
+ readSubjects(subject: string, options: ReadSubjectsOptions, signal?: AbortSignal): AsyncGenerator<string, void, void>;
19
20
  readEventType(eventType: string, signal?: AbortSignal): Promise<EventType>;
20
21
  readEventTypes(signal?: AbortSignal): AsyncGenerator<EventType, void, void>;
21
22
  }