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/dist/Client.js CHANGED
@@ -1,470 +1,110 @@
1
- import { convertCloudEventToEvent } from './convertCloudEventToEvent.js';
2
- import { HeartbeatTimeoutError } from './HeartbeatTimeoutError.js';
3
- import { isCloudEvent } from './isCloudEvent.js';
4
- import { isValidServerHeader } from './isValidServerHeader.js';
5
- import { getRawJsonElements } from './json/getRawJsonElements.js';
6
- import { getRawJsonMember } from './json/getRawJsonMember.js';
7
- import { readNdJsonStream } from './ndjson/readNdJsonStream.js';
1
+ import { detectApiVersion } from './detectApiVersion.js';
2
+ import { isAbortError } from './http/isAbortError.js';
3
+ import { waitUnlessAborted } from './http/waitUnlessAborted.js';
8
4
  import { defaultStallTimeout, maximumStallTimeout } from './stall/defaultStallTimeout.js';
9
- import { readResponseText } from './stall/readResponseText.js';
10
- import { runWithStallGuard } from './stall/runWithStallGuard.js';
11
- import { StallGuard } from './stall/StallGuard.js';
12
- import { isStreamCloudEvent } from './stream/isStreamCloudEvent.js';
13
- import { isStreamError } from './stream/isStreamError.js';
14
- import { isStreamEventType } from './stream/isStreamEventType.js';
15
- import { isStreamHeartbeat } from './stream/isStreamHeartbeat.js';
16
- import { isStreamRow } from './stream/isStreamRow.js';
17
- import { isStreamSubject } from './stream/isStreamSubject.js';
18
5
  import { hasShapeOf } from './types/hasShapeOf.js';
19
6
  import { isBoolean } from './types/isBoolean.js';
20
- import { isString } from './types/isString.js';
21
- class Client {
22
- #url;
23
- #apiToken;
24
- #stallTimeout;
25
- #getUrl(path) {
26
- return new URL(path, this.#url).toString();
7
+ import { V1Transport } from './v1/V1Transport.js';
8
+ import { V2Transport } from './v2/V2Transport.js';
9
+ // Reads a stream through the transport, unless the caller aborts while the
10
+ // client finds out which one to use, which ends the stream, as aborting it
11
+ // does at any other time.
12
+ const streamThrough = async function* (getTransport, stream) {
13
+ let transport;
14
+ try {
15
+ transport = await getTransport();
16
+ }
17
+ catch (error) {
18
+ if (isAbortError(error)) {
19
+ return;
20
+ }
21
+ throw error;
27
22
  }
23
+ yield* stream(transport);
24
+ };
25
+ class Client {
26
+ #transportOptions;
27
+ #transport;
28
+ #detection;
28
29
  constructor(url, apiToken, options = {}) {
29
30
  const stallTimeout = options.stallTimeout ?? defaultStallTimeout;
30
31
  if (!(stallTimeout >= 1 && stallTimeout <= maximumStallTimeout)) {
31
32
  throw new Error(`Stall timeout must be between 1 and ${maximumStallTimeout} milliseconds, got '${stallTimeout}'.`);
32
33
  }
33
- this.#url = url;
34
- this.#apiToken = apiToken;
35
- this.#stallTimeout = stallTimeout;
34
+ this.#transportOptions = { url, apiToken, stallTimeout };
36
35
  }
37
- async ping(signal) {
38
- const url = this.#getUrl('/api/v1/ping');
39
- const stallTimeout = this.#stallTimeout;
40
- await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
41
- const response = await stallGuard.send(url, {
42
- method: 'get',
36
+ // This is the one place that chooses between the APIs. The first request of
37
+ // a client finds out which API the server offers, and the client keeps the
38
+ // answer. Requests that start meanwhile wait for the same answer. If the
39
+ // client fails to find out, the request fails, and the next one tries again.
40
+ async #getTransport(signal) {
41
+ if (this.#transport !== undefined) {
42
+ return this.#transport;
43
+ }
44
+ signal?.throwIfAborted();
45
+ if (this.#detection === undefined) {
46
+ const detection = detectApiVersion(this.#transportOptions).then(apiVersion => {
47
+ this.#transport =
48
+ apiVersion === 'v2'
49
+ ? new V2Transport(this.#transportOptions)
50
+ : new V1Transport(this.#transportOptions);
51
+ return this.#transport;
43
52
  });
44
- if (!isValidServerHeader(response)) {
45
- throw new Error('Server must be EventSourcingDB.');
46
- }
47
- if (response.status !== 200) {
48
- throw new Error(`Failed to ping, got HTTP status code '${response.status}', expected '200'.`);
49
- }
50
- const responseBody = JSON.parse(await readResponseText(response, stallGuard));
51
- if (!hasShapeOf(responseBody, {
52
- type: isString,
53
- })) {
54
- throw new Error('Failed to parse response.');
55
- }
56
- const eventType = 'io.eventsourcingdb.api.ping-received';
57
- if (responseBody.type !== eventType) {
58
- throw new Error('Failed to ping.');
59
- }
60
- });
53
+ const forget = () => {
54
+ if (this.#detection === detection) {
55
+ this.#detection = undefined;
56
+ }
57
+ };
58
+ detection.then(forget, forget);
59
+ this.#detection = detection;
60
+ }
61
+ return await waitUnlessAborted(this.#detection, signal);
62
+ }
63
+ async ping(signal) {
64
+ const transport = await this.#getTransport(signal);
65
+ await transport.ping(signal);
61
66
  }
62
67
  async verifyApiToken(signal) {
63
- const url = this.#getUrl('/api/v1/verify-api-token');
64
- const stallTimeout = this.#stallTimeout;
65
- await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
66
- const response = await stallGuard.send(url, {
67
- method: 'post',
68
- headers: {
69
- authorization: `Bearer ${this.#apiToken}`,
70
- },
71
- });
72
- if (!isValidServerHeader(response)) {
73
- throw new Error('Server must be EventSourcingDB.');
74
- }
75
- if (response.status !== 200) {
76
- throw new Error(`Failed to verify API token, got HTTP status code '${response.status}', expected '200'.`);
77
- }
78
- const responseBody = JSON.parse(await readResponseText(response, stallGuard));
79
- if (!hasShapeOf(responseBody, {
80
- type: isString,
81
- })) {
82
- throw new Error('Failed to parse response.');
83
- }
84
- const eventType = 'io.eventsourcingdb.api.api-token-verified';
85
- if (responseBody.type !== eventType) {
86
- throw new Error('Failed to verify API token.');
87
- }
88
- });
68
+ const transport = await this.#getTransport(signal);
69
+ await transport.verifyApiToken(signal);
89
70
  }
71
+ // A write that fails once it may have reached the server fails with an
72
+ // OutcomeUnknownError, since it may have been stored nonetheless. This
73
+ // includes aborting the signal once the write has been sent.
90
74
  async writeEvents(events, preconditions = [], signal) {
91
- const url = this.#getUrl('/api/v1/write-events');
92
- const stallTimeout = this.#stallTimeout;
93
- return await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
94
- const response = await stallGuard.send(url, {
95
- method: 'post',
96
- headers: {
97
- authorization: `Bearer ${this.#apiToken}`,
98
- 'content-type': 'application/json',
99
- },
100
- body: JSON.stringify({
101
- events,
102
- preconditions,
103
- }),
104
- });
105
- if (!isValidServerHeader(response)) {
106
- throw new Error('Server must be EventSourcingDB.');
107
- }
108
- if (response.status !== 200) {
109
- throw new Error(`Failed to write events, got HTTP status code '${response.status}', expected '200'.`);
110
- }
111
- const responseText = await readResponseText(response, stallGuard);
112
- const responseBody = JSON.parse(responseText);
113
- if (!Array.isArray(responseBody)) {
114
- throw new Error('Failed to parse response.');
115
- }
116
- const responseItemTexts = getRawJsonElements(responseText);
117
- const writtenEvents = responseBody.map((item, index) => {
118
- if (!isCloudEvent(item)) {
119
- throw new Error('Failed to parse response item.');
120
- }
121
- const event = convertCloudEventToEvent(item, responseItemTexts[index]);
122
- return event;
123
- });
124
- return writtenEvents;
125
- });
75
+ const transport = await this.#getTransport(signal);
76
+ return await transport.writeEvents(events, preconditions, signal);
126
77
  }
127
78
  readEvents(subject, options, signal) {
128
- const url = this.#getUrl('/api/v1/read-events');
129
- const apiToken = this.#apiToken;
130
- const stallTimeout = this.#stallTimeout;
131
- return (async function* () {
132
- const stallGuard = new StallGuard(stallTimeout, signal);
133
- try {
134
- stallGuard.restart();
135
- const response = await stallGuard.send(url, {
136
- method: 'post',
137
- headers: {
138
- authorization: `Bearer ${apiToken}`,
139
- 'content-type': 'application/json',
140
- },
141
- body: JSON.stringify({
142
- subject,
143
- options,
144
- }),
145
- });
146
- if (!isValidServerHeader(response)) {
147
- const serverHeader = response.headers.get('Server');
148
- if (!serverHeader) {
149
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
150
- }
151
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
152
- }
153
- if (response.status !== 200) {
154
- throw new Error(`Failed to read events, got HTTP status code '${response.status}', expected '200'.`);
155
- }
156
- if (!response.body) {
157
- throw new Error('Failed to read events.');
158
- }
159
- for await (const { value: line, text } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
160
- if (isStreamError(line)) {
161
- throw new Error(`${line.payload.error}.`);
162
- }
163
- if (isStreamCloudEvent(line)) {
164
- const event = convertCloudEventToEvent(line.payload, getRawJsonMember(text, 'payload'));
165
- yield event;
166
- continue;
167
- }
168
- throw new Error('Failed to read events.');
169
- }
170
- stallGuard.throwIfStalled();
171
- }
172
- catch (error) {
173
- stallGuard.throwIfStalled();
174
- if (error instanceof DOMException && error.name === 'AbortError') {
175
- return;
176
- }
177
- throw error;
178
- }
179
- finally {
180
- stallGuard.dispose({ abortRequest: true });
181
- }
182
- })();
79
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.readEvents(subject, options, signal));
183
80
  }
184
81
  runEventQlQuery(query, signal) {
185
- const url = this.#getUrl('/api/v1/run-eventql-query');
186
- const apiToken = this.#apiToken;
187
- const stallTimeout = this.#stallTimeout;
188
- return (async function* () {
189
- const stallGuard = new StallGuard(stallTimeout, signal);
190
- try {
191
- stallGuard.restart();
192
- const response = await stallGuard.send(url, {
193
- method: 'post',
194
- headers: {
195
- authorization: `Bearer ${apiToken}`,
196
- 'content-type': 'application/json',
197
- },
198
- body: JSON.stringify({ query }),
199
- });
200
- // Once the headers have arrived, the server sends a heartbeat every
201
- // second, so from now on a stall means that heartbeats are missing.
202
- stallGuard.onStall(() => new HeartbeatTimeoutError(stallTimeout));
203
- if (!isValidServerHeader(response)) {
204
- const serverHeader = response.headers.get('Server');
205
- if (!serverHeader) {
206
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
207
- }
208
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
209
- }
210
- if (response.status !== 200) {
211
- throw new Error(`Failed to run EventQL query, got HTTP status code '${response.status}', expected '200'.`);
212
- }
213
- if (!response.body) {
214
- throw new Error('Failed to run EventQL query.');
215
- }
216
- for await (const { value: line } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
217
- if (isStreamHeartbeat(line)) {
218
- continue;
219
- }
220
- if (isStreamError(line)) {
221
- throw new Error(`${line.payload.error}.`);
222
- }
223
- if (isStreamRow(line)) {
224
- const row = line.payload;
225
- yield row;
226
- continue;
227
- }
228
- throw new Error('Failed to run EventQL query.');
229
- }
230
- stallGuard.throwIfStalled();
231
- }
232
- catch (error) {
233
- stallGuard.throwIfStalled();
234
- if (error instanceof DOMException && error.name === 'AbortError') {
235
- return;
236
- }
237
- throw error;
238
- }
239
- finally {
240
- stallGuard.dispose({ abortRequest: true });
241
- }
242
- })();
82
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.runEventQlQuery(query, signal));
243
83
  }
244
84
  observeEvents(subject, options, signal) {
245
- const url = this.#getUrl('/api/v1/observe-events');
246
- const apiToken = this.#apiToken;
247
- const stallTimeout = this.#stallTimeout;
248
- return (async function* () {
249
- const stallGuard = new StallGuard(stallTimeout, signal);
250
- try {
251
- stallGuard.restart();
252
- const response = await stallGuard.send(url, {
253
- method: 'post',
254
- headers: {
255
- authorization: `Bearer ${apiToken}`,
256
- 'content-type': 'application/json',
257
- },
258
- body: JSON.stringify({
259
- subject,
260
- options,
261
- }),
262
- });
263
- // Once the headers have arrived, the server sends a heartbeat every
264
- // second, so from now on a stall means that heartbeats are missing.
265
- stallGuard.onStall(() => new HeartbeatTimeoutError(stallTimeout));
266
- if (!isValidServerHeader(response)) {
267
- const serverHeader = response.headers.get('Server');
268
- if (!serverHeader) {
269
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
270
- }
271
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
272
- }
273
- if (response.status !== 200) {
274
- throw new Error(`Failed to observe events, got HTTP status code '${response.status}', expected '200'.`);
275
- }
276
- if (!response.body) {
277
- throw new Error('Failed to observe events.');
278
- }
279
- for await (const { value: line, text } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
280
- if (isStreamHeartbeat(line)) {
281
- continue;
282
- }
283
- if (isStreamError(line)) {
284
- throw new Error(`${line.payload.error}.`);
285
- }
286
- if (isStreamCloudEvent(line)) {
287
- const event = convertCloudEventToEvent(line.payload, getRawJsonMember(text, 'payload'));
288
- yield event;
289
- continue;
290
- }
291
- throw new Error('Failed to observe events.');
292
- }
293
- stallGuard.throwIfStalled();
294
- }
295
- catch (error) {
296
- stallGuard.throwIfStalled();
297
- if (error instanceof DOMException && error.name === 'AbortError') {
298
- return;
299
- }
300
- throw error;
301
- }
302
- finally {
303
- stallGuard.dispose({ abortRequest: true });
304
- }
305
- })();
85
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.observeEvents(subject, options, signal));
306
86
  }
87
+ // Registering an event schema is a write, so it fails like writeEvents does.
307
88
  async registerEventSchema(eventType, schema, signal) {
308
- const url = this.#getUrl('/api/v1/register-event-schema');
309
- const stallTimeout = this.#stallTimeout;
310
- await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
311
- const response = await stallGuard.send(url, {
312
- method: 'post',
313
- headers: {
314
- authorization: `Bearer ${this.#apiToken}`,
315
- 'content-type': 'application/json',
316
- },
317
- body: JSON.stringify({
318
- eventType,
319
- schema,
320
- }),
321
- });
322
- if (!isValidServerHeader(response)) {
323
- throw new Error('Server must be EventSourcingDB.');
324
- }
325
- if (response.status !== 200) {
326
- throw new Error(`Failed to register event schema, got HTTP status code '${response.status}', expected '200'.`);
327
- }
328
- });
89
+ const transport = await this.#getTransport(signal);
90
+ await transport.registerEventSchema(eventType, schema, signal);
329
91
  }
330
- readSubjects(baseSubject, signal) {
331
- const url = this.#getUrl('/api/v1/read-subjects');
332
- const apiToken = this.#apiToken;
333
- const stallTimeout = this.#stallTimeout;
334
- return (async function* () {
335
- const stallGuard = new StallGuard(stallTimeout, signal);
336
- try {
337
- stallGuard.restart();
338
- const response = await stallGuard.send(url, {
339
- method: 'post',
340
- headers: {
341
- authorization: `Bearer ${apiToken}`,
342
- 'content-type': 'application/json',
343
- },
344
- body: JSON.stringify({
345
- baseSubject,
346
- }),
347
- });
348
- if (!isValidServerHeader(response)) {
349
- const serverHeader = response.headers.get('Server');
350
- if (!serverHeader) {
351
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
352
- }
353
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
354
- }
355
- if (response.status !== 200) {
356
- throw new Error(`Failed to read subjects, got HTTP status code '${response.status}', expected '200'.`);
357
- }
358
- if (!response.body) {
359
- throw new Error('Failed to read subjects.');
360
- }
361
- for await (const { value: line } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
362
- if (isStreamError(line)) {
363
- throw new Error(`${line.payload.error}.`);
364
- }
365
- if (isStreamSubject(line)) {
366
- yield line.payload.subject;
367
- continue;
368
- }
369
- throw new Error('Failed to read subjects.');
370
- }
371
- stallGuard.throwIfStalled();
372
- }
373
- catch (error) {
374
- stallGuard.throwIfStalled();
375
- if (error instanceof DOMException && error.name === 'AbortError') {
376
- return;
377
- }
378
- throw error;
379
- }
380
- finally {
381
- stallGuard.dispose({ abortRequest: true });
382
- }
383
- })();
92
+ readSubjects(subject, options, signal) {
93
+ // The option decides whether to read the subject alone or with all the
94
+ // subjects below it, so it has no default, and a caller without types has
95
+ // to set it as well.
96
+ if (!hasShapeOf(options, { recursive: isBoolean })) {
97
+ throw new Error("Option 'recursive' must be set to true or false.");
98
+ }
99
+ const { recursive } = options;
100
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.readSubjects(subject, { recursive }, signal));
384
101
  }
385
102
  async readEventType(eventType, signal) {
386
- const url = this.#getUrl('/api/v1/read-event-type');
387
- const stallTimeout = this.#stallTimeout;
388
- return await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
389
- const response = await stallGuard.send(url, {
390
- method: 'post',
391
- headers: {
392
- authorization: `Bearer ${this.#apiToken}`,
393
- 'content-type': 'application/json',
394
- },
395
- body: JSON.stringify({
396
- eventType,
397
- }),
398
- });
399
- if (!isValidServerHeader(response)) {
400
- throw new Error('Server must be EventSourcingDB.');
401
- }
402
- if (response.status !== 200) {
403
- throw new Error(`Failed to read event type, got HTTP status code '${response.status}', expected '200'.`);
404
- }
405
- const responseBody = JSON.parse(await readResponseText(response, stallGuard));
406
- if (!hasShapeOf(responseBody, {
407
- eventType: isString,
408
- isPhantom: isBoolean,
409
- })) {
410
- throw new Error('Failed to parse response.');
411
- }
412
- return responseBody;
413
- });
103
+ const transport = await this.#getTransport(signal);
104
+ return await transport.readEventType(eventType, signal);
414
105
  }
415
106
  readEventTypes(signal) {
416
- const url = this.#getUrl('/api/v1/read-event-types');
417
- const apiToken = this.#apiToken;
418
- const stallTimeout = this.#stallTimeout;
419
- return (async function* () {
420
- const stallGuard = new StallGuard(stallTimeout, signal);
421
- try {
422
- stallGuard.restart();
423
- const response = await stallGuard.send(url, {
424
- method: 'post',
425
- headers: {
426
- authorization: `Bearer ${apiToken}`,
427
- },
428
- });
429
- if (!isValidServerHeader(response)) {
430
- const serverHeader = response.headers.get('Server');
431
- if (!serverHeader) {
432
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
433
- }
434
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
435
- }
436
- if (response.status !== 200) {
437
- throw new Error(`Failed to read event types, got HTTP status code '${response.status}', expected '200'.`);
438
- }
439
- if (!response.body) {
440
- throw new Error('Failed to read event types.');
441
- }
442
- for await (const { value: line } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
443
- if (isStreamHeartbeat(line)) {
444
- continue;
445
- }
446
- if (isStreamError(line)) {
447
- throw new Error(`${line.payload.error}.`);
448
- }
449
- if (isStreamEventType(line)) {
450
- yield line.payload;
451
- continue;
452
- }
453
- throw new Error('Failed to read event types.');
454
- }
455
- stallGuard.throwIfStalled();
456
- }
457
- catch (error) {
458
- stallGuard.throwIfStalled();
459
- if (error instanceof DOMException && error.name === 'AbortError') {
460
- return;
461
- }
462
- throw error;
463
- }
464
- finally {
465
- stallGuard.dispose({ abortRequest: true });
466
- }
467
- })();
107
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.readEventTypes(signal));
468
108
  }
469
109
  }
470
110
  export { Client };
@@ -0,0 +1,17 @@
1
+ interface DbApiErrorOptions {
2
+ status: number;
3
+ description: string;
4
+ code?: string;
5
+ details?: Record<string, unknown>;
6
+ retryAfter?: number;
7
+ }
8
+ declare class DbApiError extends Error {
9
+ readonly code: string | undefined;
10
+ readonly description: string;
11
+ readonly details: Record<string, unknown>;
12
+ readonly status: number;
13
+ readonly retryAfter: number | undefined;
14
+ constructor({ status, description, code, details, retryAfter }: DbApiErrorOptions);
15
+ }
16
+ export type { DbApiErrorOptions };
17
+ export { DbApiError };
@@ -0,0 +1,24 @@
1
+ // An error that the server reports, whether as an error item of the v2 API, as
2
+ // a status other than 200 with a text, or as an error of the v1 API.
3
+ class DbApiError extends Error {
4
+ // The URL that names the error, exactly as the server sends it, which the
5
+ // constants of ErrorCode hold. It is undefined for an error without a code.
6
+ code;
7
+ description;
8
+ details;
9
+ // The HTTP status of the response, which is 200 for an error item.
10
+ status;
11
+ // The seconds after which the server accepts requests again, which only a
12
+ // response with the status 429 may name.
13
+ retryAfter;
14
+ constructor({ status, description, code, details = {}, retryAfter }) {
15
+ super(description === '' ? `The server answered with HTTP status code '${status}'.` : description);
16
+ this.name = 'DbApiError';
17
+ this.code = code;
18
+ this.description = description;
19
+ this.details = details;
20
+ this.status = status;
21
+ this.retryAfter = retryAfter;
22
+ }
23
+ }
24
+ export { DbApiError };