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/dist/Client.js CHANGED
@@ -1,485 +1,110 @@
1
- import { convertCloudEventToEvent } from './convertCloudEventToEvent.js';
2
- import { heartbeatTimeout } from './heartbeatTimeout.js';
3
- import { isCloudEvent } from './isCloudEvent.js';
4
- import { isValidServerHeader } from './isValidServerHeader.js';
5
- import { readNdJsonStream } from './ndjson/readNdJsonStream.js';
6
- import { isStreamCloudEvent } from './stream/isStreamCloudEvent.js';
7
- import { isStreamError } from './stream/isStreamError.js';
8
- import { isStreamEventType } from './stream/isStreamEventType.js';
9
- import { isStreamHeartbeat } from './stream/isStreamHeartbeat.js';
10
- import { isStreamRow } from './stream/isStreamRow.js';
11
- import { isStreamSubject } from './stream/isStreamSubject.js';
1
+ import { detectApiVersion } from './detectApiVersion.js';
2
+ import { isAbortError } from './http/isAbortError.js';
3
+ import { waitUnlessAborted } from './http/waitUnlessAborted.js';
4
+ import { defaultStallTimeout, maximumStallTimeout } from './stall/defaultStallTimeout.js';
12
5
  import { hasShapeOf } from './types/hasShapeOf.js';
13
6
  import { isBoolean } from './types/isBoolean.js';
14
- import { isString } from './types/isString.js';
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;
22
+ }
23
+ yield* stream(transport);
24
+ };
15
25
  class Client {
16
- #url;
17
- #apiToken;
18
- #getUrl(path) {
19
- return new URL(path, this.#url).toString();
26
+ #transportOptions;
27
+ #transport;
28
+ #detection;
29
+ constructor(url, apiToken, options = {}) {
30
+ const stallTimeout = options.stallTimeout ?? defaultStallTimeout;
31
+ if (!(stallTimeout >= 1 && stallTimeout <= maximumStallTimeout)) {
32
+ throw new Error(`Stall timeout must be between 1 and ${maximumStallTimeout} milliseconds, got '${stallTimeout}'.`);
33
+ }
34
+ this.#transportOptions = { url, apiToken, stallTimeout };
20
35
  }
21
- constructor(url, apiToken) {
22
- this.#url = url;
23
- this.#apiToken = apiToken;
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;
52
+ });
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);
24
62
  }
25
- async ping() {
26
- const url = this.#getUrl('/api/v1/ping');
27
- const response = await fetch(url, {
28
- method: 'get',
29
- });
30
- if (!isValidServerHeader(response)) {
31
- throw new Error('Server must be EventSourcingDB.');
32
- }
33
- if (response.status !== 200) {
34
- throw new Error(`Failed to ping, got HTTP status code '${response.status}', expected '200'.`);
35
- }
36
- const responseBody = await response.json();
37
- if (!hasShapeOf(responseBody, {
38
- type: isString,
39
- })) {
40
- throw new Error('Failed to parse response.');
41
- }
42
- const eventType = 'io.eventsourcingdb.api.ping-received';
43
- if (responseBody.type !== eventType) {
44
- throw new Error('Failed to ping.');
45
- }
63
+ async ping(signal) {
64
+ const transport = await this.#getTransport(signal);
65
+ await transport.ping(signal);
46
66
  }
47
- async verifyApiToken() {
48
- const url = this.#getUrl('/api/v1/verify-api-token');
49
- const response = await fetch(url, {
50
- method: 'post',
51
- headers: {
52
- authorization: `Bearer ${this.#apiToken}`,
53
- },
54
- });
55
- if (!isValidServerHeader(response)) {
56
- throw new Error('Server must be EventSourcingDB.');
57
- }
58
- if (response.status !== 200) {
59
- throw new Error(`Failed to verify API token, got HTTP status code '${response.status}', expected '200'.`);
60
- }
61
- const responseBody = await response.json();
62
- if (!hasShapeOf(responseBody, {
63
- type: isString,
64
- })) {
65
- throw new Error('Failed to parse response.');
66
- }
67
- const eventType = 'io.eventsourcingdb.api.api-token-verified';
68
- if (responseBody.type !== eventType) {
69
- throw new Error('Failed to verify API token.');
70
- }
67
+ async verifyApiToken(signal) {
68
+ const transport = await this.#getTransport(signal);
69
+ await transport.verifyApiToken(signal);
71
70
  }
72
- async writeEvents(events, preconditions = []) {
73
- const url = this.#getUrl('/api/v1/write-events');
74
- const response = await fetch(url, {
75
- method: 'post',
76
- headers: {
77
- authorization: `Bearer ${this.#apiToken}`,
78
- 'content-type': 'application/json',
79
- },
80
- body: JSON.stringify({
81
- events,
82
- preconditions,
83
- }),
84
- });
85
- if (!isValidServerHeader(response)) {
86
- throw new Error('Server must be EventSourcingDB.');
87
- }
88
- if (response.status !== 200) {
89
- throw new Error(`Failed to write events, got HTTP status code '${response.status}', expected '200'.`);
90
- }
91
- const responseBody = await response.json();
92
- if (!Array.isArray(responseBody)) {
93
- throw new Error('Failed to parse response.');
94
- }
95
- const writtenEvents = responseBody.map(item => {
96
- if (!isCloudEvent(item)) {
97
- throw new Error('Failed to parse response item.');
98
- }
99
- const event = convertCloudEventToEvent(item);
100
- return event;
101
- });
102
- return writtenEvents;
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.
74
+ async writeEvents(events, preconditions = [], signal) {
75
+ const transport = await this.#getTransport(signal);
76
+ return await transport.writeEvents(events, preconditions, signal);
103
77
  }
104
78
  readEvents(subject, options, signal) {
105
- const url = this.#getUrl('/api/v1/read-events');
106
- const apiToken = this.#apiToken;
107
- return (async function* () {
108
- const internalAbortController = new AbortController();
109
- const combinedSignal = signal ?? internalAbortController.signal;
110
- const shouldAbortInternally = !signal;
111
- let removeAbortListener;
112
- if (signal && !signal.aborted) {
113
- const onAbort = () => internalAbortController.abort();
114
- signal.addEventListener('abort', onAbort, { once: true });
115
- removeAbortListener = () => signal.removeEventListener('abort', onAbort);
116
- }
117
- try {
118
- const response = await fetch(url, {
119
- method: 'post',
120
- headers: {
121
- authorization: `Bearer ${apiToken}`,
122
- 'content-type': 'application/json',
123
- },
124
- body: JSON.stringify({
125
- subject,
126
- options,
127
- }),
128
- signal: combinedSignal,
129
- });
130
- if (!isValidServerHeader(response)) {
131
- const serverHeader = response.headers.get('Server');
132
- if (!serverHeader) {
133
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
134
- }
135
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
136
- }
137
- if (response.status !== 200) {
138
- throw new Error(`Failed to read events, got HTTP status code '${response.status}', expected '200'.`);
139
- }
140
- if (!response.body) {
141
- throw new Error('Failed to read events.');
142
- }
143
- for await (const line of readNdJsonStream(response.body, combinedSignal)) {
144
- if (isStreamError(line)) {
145
- throw new Error(`${line.payload.error}.`);
146
- }
147
- if (isStreamCloudEvent(line)) {
148
- const event = convertCloudEventToEvent(line.payload);
149
- yield event;
150
- continue;
151
- }
152
- throw new Error('Failed to read events.');
153
- }
154
- }
155
- catch (error) {
156
- if (error instanceof DOMException && error.name === 'AbortError') {
157
- return;
158
- }
159
- throw error;
160
- }
161
- finally {
162
- if (removeAbortListener) {
163
- removeAbortListener();
164
- }
165
- if (shouldAbortInternally) {
166
- internalAbortController.abort();
167
- }
168
- }
169
- })();
79
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.readEvents(subject, options, signal));
170
80
  }
171
81
  runEventQlQuery(query, signal) {
172
- const url = this.#getUrl('/api/v1/run-eventql-query');
173
- const apiToken = this.#apiToken;
174
- return (async function* () {
175
- const internalAbortController = new AbortController();
176
- const combinedSignal = signal ?? internalAbortController.signal;
177
- const shouldAbortInternally = !signal;
178
- let removeAbortListener;
179
- if (signal && !signal.aborted) {
180
- const onAbort = () => internalAbortController.abort();
181
- signal.addEventListener('abort', onAbort, { once: true });
182
- removeAbortListener = () => signal.removeEventListener('abort', onAbort);
183
- }
184
- try {
185
- const response = await fetch(url, {
186
- method: 'post',
187
- headers: {
188
- authorization: `Bearer ${apiToken}`,
189
- 'content-type': 'application/json',
190
- },
191
- body: JSON.stringify({ query }),
192
- signal: combinedSignal,
193
- });
194
- if (!isValidServerHeader(response)) {
195
- const serverHeader = response.headers.get('Server');
196
- if (!serverHeader) {
197
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
198
- }
199
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
200
- }
201
- if (response.status !== 200) {
202
- throw new Error(`Failed to run EventQL query, got HTTP status code '${response.status}', expected '200'.`);
203
- }
204
- if (!response.body) {
205
- throw new Error('Failed to run EventQL query.');
206
- }
207
- for await (const line of readNdJsonStream(response.body, combinedSignal, heartbeatTimeout.milliseconds)) {
208
- if (isStreamHeartbeat(line)) {
209
- continue;
210
- }
211
- if (isStreamError(line)) {
212
- throw new Error(`${line.payload.error}.`);
213
- }
214
- if (isStreamRow(line)) {
215
- const row = line.payload;
216
- yield row;
217
- continue;
218
- }
219
- throw new Error('Failed to run EventQL query.');
220
- }
221
- }
222
- catch (error) {
223
- if (error instanceof DOMException && error.name === 'AbortError') {
224
- return;
225
- }
226
- throw error;
227
- }
228
- finally {
229
- if (removeAbortListener) {
230
- removeAbortListener();
231
- }
232
- if (shouldAbortInternally) {
233
- internalAbortController.abort();
234
- }
235
- }
236
- })();
82
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.runEventQlQuery(query, signal));
237
83
  }
238
84
  observeEvents(subject, options, signal) {
239
- const url = this.#getUrl('/api/v1/observe-events');
240
- const apiToken = this.#apiToken;
241
- return (async function* () {
242
- const internalAbortController = new AbortController();
243
- const combinedSignal = signal ?? internalAbortController.signal;
244
- const shouldAbortInternally = !signal;
245
- let removeAbortListener;
246
- if (signal && !signal.aborted) {
247
- const onAbort = () => internalAbortController.abort();
248
- signal.addEventListener('abort', onAbort, { once: true });
249
- removeAbortListener = () => signal.removeEventListener('abort', onAbort);
250
- }
251
- try {
252
- const response = await fetch(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
- signal: combinedSignal,
263
- });
264
- if (!isValidServerHeader(response)) {
265
- const serverHeader = response.headers.get('Server');
266
- if (!serverHeader) {
267
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
268
- }
269
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
270
- }
271
- if (response.status !== 200) {
272
- throw new Error(`Failed to observe events, got HTTP status code '${response.status}', expected '200'.`);
273
- }
274
- if (!response.body) {
275
- throw new Error('Failed to observe events.');
276
- }
277
- for await (const line of readNdJsonStream(response.body, combinedSignal, heartbeatTimeout.milliseconds)) {
278
- if (isStreamHeartbeat(line)) {
279
- continue;
280
- }
281
- if (isStreamError(line)) {
282
- throw new Error(`${line.payload.error}.`);
283
- }
284
- if (isStreamCloudEvent(line)) {
285
- const event = convertCloudEventToEvent(line.payload);
286
- yield event;
287
- continue;
288
- }
289
- throw new Error('Failed to observe events.');
290
- }
291
- }
292
- catch (error) {
293
- if (error instanceof DOMException && error.name === 'AbortError') {
294
- return;
295
- }
296
- throw error;
297
- }
298
- finally {
299
- if (removeAbortListener) {
300
- removeAbortListener();
301
- }
302
- if (shouldAbortInternally) {
303
- internalAbortController.abort();
304
- }
305
- }
306
- })();
85
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.observeEvents(subject, options, signal));
307
86
  }
308
- async registerEventSchema(eventType, schema) {
309
- const url = this.#getUrl('/api/v1/register-event-schema');
310
- const response = await fetch(url, {
311
- method: 'post',
312
- headers: {
313
- authorization: `Bearer ${this.#apiToken}`,
314
- 'content-type': 'application/json',
315
- },
316
- body: JSON.stringify({
317
- eventType,
318
- schema,
319
- }),
320
- });
321
- if (!isValidServerHeader(response)) {
322
- throw new Error('Server must be EventSourcingDB.');
323
- }
324
- if (response.status !== 200) {
325
- throw new Error(`Failed to register event schema, got HTTP status code '${response.status}', expected '200'.`);
326
- }
87
+ // Registering an event schema is a write, so it fails like writeEvents does.
88
+ async registerEventSchema(eventType, schema, signal) {
89
+ const transport = await this.#getTransport(signal);
90
+ await transport.registerEventSchema(eventType, schema, signal);
327
91
  }
328
- readSubjects(baseSubject, signal) {
329
- const url = this.#getUrl('/api/v1/read-subjects');
330
- const apiToken = this.#apiToken;
331
- return (async function* () {
332
- const internalAbortController = new AbortController();
333
- const combinedSignal = signal ?? internalAbortController.signal;
334
- const shouldAbortInternally = !signal;
335
- let removeAbortListener;
336
- if (signal && !signal.aborted) {
337
- const onAbort = () => internalAbortController.abort();
338
- signal.addEventListener('abort', onAbort, { once: true });
339
- removeAbortListener = () => signal.removeEventListener('abort', onAbort);
340
- }
341
- try {
342
- const response = await fetch(url, {
343
- method: 'post',
344
- headers: {
345
- authorization: `Bearer ${apiToken}`,
346
- 'content-type': 'application/json',
347
- },
348
- body: JSON.stringify({
349
- baseSubject,
350
- }),
351
- signal: combinedSignal,
352
- });
353
- if (!isValidServerHeader(response)) {
354
- const serverHeader = response.headers.get('Server');
355
- if (!serverHeader) {
356
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
357
- }
358
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
359
- }
360
- if (response.status !== 200) {
361
- throw new Error(`Failed to read subjects, got HTTP status code '${response.status}', expected '200'.`);
362
- }
363
- if (!response.body) {
364
- throw new Error('Failed to read subjects.');
365
- }
366
- for await (const line of readNdJsonStream(response.body, combinedSignal)) {
367
- if (isStreamError(line)) {
368
- throw new Error(`${line.payload.error}.`);
369
- }
370
- if (isStreamSubject(line)) {
371
- yield line.payload.subject;
372
- continue;
373
- }
374
- throw new Error('Failed to read subjects.');
375
- }
376
- }
377
- catch (error) {
378
- if (error instanceof DOMException && error.name === 'AbortError') {
379
- return;
380
- }
381
- throw error;
382
- }
383
- finally {
384
- if (removeAbortListener) {
385
- removeAbortListener();
386
- }
387
- if (shouldAbortInternally) {
388
- internalAbortController.abort();
389
- }
390
- }
391
- })();
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));
392
101
  }
393
- async readEventType(eventType) {
394
- const url = this.#getUrl('/api/v1/read-event-type');
395
- const response = await fetch(url, {
396
- method: 'post',
397
- headers: {
398
- authorization: `Bearer ${this.#apiToken}`,
399
- 'content-type': 'application/json',
400
- },
401
- body: JSON.stringify({
402
- eventType,
403
- }),
404
- });
405
- if (!isValidServerHeader(response)) {
406
- throw new Error('Server must be EventSourcingDB.');
407
- }
408
- if (response.status !== 200) {
409
- throw new Error(`Failed to read event type, got HTTP status code '${response.status}', expected '200'.`);
410
- }
411
- const responseBody = await response.json();
412
- if (!hasShapeOf(responseBody, {
413
- eventType: isString,
414
- isPhantom: isBoolean,
415
- })) {
416
- throw new Error('Failed to parse response.');
417
- }
418
- return responseBody;
102
+ async readEventType(eventType, signal) {
103
+ const transport = await this.#getTransport(signal);
104
+ return await transport.readEventType(eventType, signal);
419
105
  }
420
106
  readEventTypes(signal) {
421
- const url = this.#getUrl('/api/v1/read-event-types');
422
- const apiToken = this.#apiToken;
423
- return (async function* () {
424
- const internalAbortController = new AbortController();
425
- const combinedSignal = signal ?? internalAbortController.signal;
426
- const shouldAbortInternally = !signal;
427
- let removeAbortListener;
428
- if (signal && !signal.aborted) {
429
- const onAbort = () => internalAbortController.abort();
430
- signal.addEventListener('abort', onAbort, { once: true });
431
- removeAbortListener = () => signal.removeEventListener('abort', onAbort);
432
- }
433
- try {
434
- const response = await fetch(url, {
435
- method: 'post',
436
- headers: {
437
- authorization: `Bearer ${apiToken}`,
438
- },
439
- signal: combinedSignal,
440
- });
441
- if (!isValidServerHeader(response)) {
442
- const serverHeader = response.headers.get('Server');
443
- if (!serverHeader) {
444
- throw new Error('Server must be EventSourcingDB, but Server header is missing.');
445
- }
446
- throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
447
- }
448
- if (response.status !== 200) {
449
- throw new Error(`Failed to read event types, got HTTP status code '${response.status}', expected '200'.`);
450
- }
451
- if (!response.body) {
452
- throw new Error('Failed to read event types.');
453
- }
454
- for await (const line of readNdJsonStream(response.body, combinedSignal)) {
455
- if (isStreamHeartbeat(line)) {
456
- continue;
457
- }
458
- if (isStreamError(line)) {
459
- throw new Error(`${line.payload.error}.`);
460
- }
461
- if (isStreamEventType(line)) {
462
- yield line.payload;
463
- continue;
464
- }
465
- throw new Error('Failed to read event types.');
466
- }
467
- }
468
- catch (error) {
469
- if (error instanceof DOMException && error.name === 'AbortError') {
470
- return;
471
- }
472
- throw error;
473
- }
474
- finally {
475
- if (removeAbortListener) {
476
- removeAbortListener();
477
- }
478
- if (shouldAbortInternally) {
479
- internalAbortController.abort();
480
- }
481
- }
482
- })();
107
+ return streamThrough(async () => await this.#getTransport(signal), transport => transport.readEventTypes(signal));
483
108
  }
484
109
  }
485
110
  export { Client };
@@ -0,0 +1,4 @@
1
+ interface ClientOptions {
2
+ stallTimeout?: number;
3
+ }
4
+ export type { ClientOptions };
@@ -0,0 +1,4 @@
1
+ declare class ConnectionStalledError extends TypeError {
2
+ constructor(stallTimeout?: number, options?: ErrorOptions);
3
+ }
4
+ export { ConnectionStalledError };
@@ -0,0 +1,13 @@
1
+ import { defaultStallTimeout } from './stall/defaultStallTimeout.js';
2
+ import { formatDuration } from './stall/formatDuration.js';
3
+ // Extends TypeError, since fetch fails with a TypeError if a request stalls
4
+ // for longer than its own timeouts allow, so code that catches that keeps
5
+ // working. For the same reason, its cause carries the code of the timeout
6
+ // error of fetch for the same situation.
7
+ class ConnectionStalledError extends TypeError {
8
+ constructor(stallTimeout = defaultStallTimeout, options) {
9
+ super(`Nothing arrived from the server for ${formatDuration(stallTimeout)}.`, options);
10
+ this.name = 'ConnectionStalledError';
11
+ }
12
+ }
13
+ export { ConnectionStalledError };
@@ -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 };