eventsourcingdb 1.9.1 → 1.10.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.
- package/README.md +77 -6
- package/dist/Client.d.ts +7 -6
- package/dist/Client.js +189 -204
- package/dist/ClientOptions.d.ts +4 -0
- package/dist/ClientOptions.js +1 -0
- package/dist/ConnectionStalledError.d.ts +4 -0
- package/dist/ConnectionStalledError.js +13 -0
- package/dist/Event.d.ts +2 -1
- package/dist/Event.js +58 -11
- package/dist/HashVerificationFailedError.d.ts +4 -0
- package/dist/HashVerificationFailedError.js +7 -0
- package/dist/HeartbeatTimeoutError.d.ts +1 -1
- package/dist/HeartbeatTimeoutError.js +4 -2
- package/dist/SignatureVerificationFailedError.d.ts +4 -0
- package/dist/SignatureVerificationFailedError.js +7 -0
- package/dist/UnsupportedHashVersionError.d.ts +4 -0
- package/dist/UnsupportedHashVersionError.js +7 -0
- package/dist/convertCloudEventToEvent.d.ts +1 -1
- package/dist/convertCloudEventToEvent.js +6 -1
- package/dist/getImageTag.d.ts +2 -0
- package/dist/getImageTag.js +13 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +5 -1
- package/dist/json/getRawJsonElements.d.ts +2 -0
- package/dist/json/getRawJsonElements.js +22 -0
- package/dist/json/getRawJsonMember.d.ts +2 -0
- package/dist/json/getRawJsonMember.js +29 -0
- package/dist/json/scanJson.d.ts +4 -0
- package/dist/json/scanJson.js +65 -0
- package/dist/ndjson/readNdJsonStream.d.ts +7 -1
- package/dist/ndjson/readNdJsonStream.js +7 -28
- package/dist/stall/StallGuard.d.ts +14 -0
- package/dist/stall/StallGuard.js +83 -0
- package/dist/stall/createFetchTimeoutError.d.ts +2 -0
- package/dist/stall/createFetchTimeoutError.js +16 -0
- package/dist/stall/defaultStallTimeout.d.ts +3 -0
- package/dist/stall/defaultStallTimeout.js +6 -0
- package/dist/stall/formatDuration.d.ts +2 -0
- package/dist/stall/formatDuration.js +9 -0
- package/dist/stall/readResponseText.d.ts +3 -0
- package/dist/stall/readResponseText.js +26 -0
- package/dist/stall/runWithStallGuard.d.ts +6 -0
- package/dist/stall/runWithStallGuard.js +18 -0
- package/package.json +3 -3
- package/dist/heartbeatTimeout.d.ts +0 -4
- package/dist/heartbeatTimeout.js +0 -6
package/dist/Client.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { convertCloudEventToEvent } from './convertCloudEventToEvent.js';
|
|
2
|
-
import {
|
|
2
|
+
import { HeartbeatTimeoutError } from './HeartbeatTimeoutError.js';
|
|
3
3
|
import { isCloudEvent } from './isCloudEvent.js';
|
|
4
4
|
import { isValidServerHeader } from './isValidServerHeader.js';
|
|
5
|
+
import { getRawJsonElements } from './json/getRawJsonElements.js';
|
|
6
|
+
import { getRawJsonMember } from './json/getRawJsonMember.js';
|
|
5
7
|
import { readNdJsonStream } from './ndjson/readNdJsonStream.js';
|
|
8
|
+
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';
|
|
6
12
|
import { isStreamCloudEvent } from './stream/isStreamCloudEvent.js';
|
|
7
13
|
import { isStreamError } from './stream/isStreamError.js';
|
|
8
14
|
import { isStreamEventType } from './stream/isStreamEventType.js';
|
|
@@ -15,107 +21,118 @@ import { isString } from './types/isString.js';
|
|
|
15
21
|
class Client {
|
|
16
22
|
#url;
|
|
17
23
|
#apiToken;
|
|
24
|
+
#stallTimeout;
|
|
18
25
|
#getUrl(path) {
|
|
19
26
|
return new URL(path, this.#url).toString();
|
|
20
27
|
}
|
|
21
|
-
constructor(url, apiToken) {
|
|
28
|
+
constructor(url, apiToken, options = {}) {
|
|
29
|
+
const stallTimeout = options.stallTimeout ?? defaultStallTimeout;
|
|
30
|
+
if (!(stallTimeout >= 1 && stallTimeout <= maximumStallTimeout)) {
|
|
31
|
+
throw new Error(`Stall timeout must be between 1 and ${maximumStallTimeout} milliseconds, got '${stallTimeout}'.`);
|
|
32
|
+
}
|
|
22
33
|
this.#url = url;
|
|
23
34
|
this.#apiToken = apiToken;
|
|
35
|
+
this.#stallTimeout = stallTimeout;
|
|
24
36
|
}
|
|
25
|
-
async ping() {
|
|
37
|
+
async ping(signal) {
|
|
26
38
|
const url = this.#getUrl('/api/v1/ping');
|
|
27
|
-
const
|
|
28
|
-
|
|
39
|
+
const stallTimeout = this.#stallTimeout;
|
|
40
|
+
await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
|
|
41
|
+
const response = await stallGuard.send(url, {
|
|
42
|
+
method: 'get',
|
|
43
|
+
});
|
|
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
|
+
}
|
|
29
60
|
});
|
|
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
|
-
}
|
|
46
61
|
}
|
|
47
|
-
async verifyApiToken() {
|
|
62
|
+
async verifyApiToken(signal) {
|
|
48
63
|
const url = this.#getUrl('/api/v1/verify-api-token');
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
88
|
});
|
|
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
|
-
}
|
|
71
89
|
}
|
|
72
|
-
async writeEvents(events, preconditions = []) {
|
|
90
|
+
async writeEvents(events, preconditions = [], signal) {
|
|
73
91
|
const url = this.#getUrl('/api/v1/write-events');
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.');
|
|
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.');
|
|
98
107
|
}
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
125
|
});
|
|
102
|
-
return writtenEvents;
|
|
103
126
|
}
|
|
104
127
|
readEvents(subject, options, signal) {
|
|
105
128
|
const url = this.#getUrl('/api/v1/read-events');
|
|
106
129
|
const apiToken = this.#apiToken;
|
|
130
|
+
const stallTimeout = this.#stallTimeout;
|
|
107
131
|
return (async function* () {
|
|
108
|
-
const
|
|
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
|
-
}
|
|
132
|
+
const stallGuard = new StallGuard(stallTimeout, signal);
|
|
117
133
|
try {
|
|
118
|
-
|
|
134
|
+
stallGuard.restart();
|
|
135
|
+
const response = await stallGuard.send(url, {
|
|
119
136
|
method: 'post',
|
|
120
137
|
headers: {
|
|
121
138
|
authorization: `Bearer ${apiToken}`,
|
|
@@ -125,7 +142,6 @@ class Client {
|
|
|
125
142
|
subject,
|
|
126
143
|
options,
|
|
127
144
|
}),
|
|
128
|
-
signal: combinedSignal,
|
|
129
145
|
});
|
|
130
146
|
if (!isValidServerHeader(response)) {
|
|
131
147
|
const serverHeader = response.headers.get('Server');
|
|
@@ -140,57 +156,50 @@ class Client {
|
|
|
140
156
|
if (!response.body) {
|
|
141
157
|
throw new Error('Failed to read events.');
|
|
142
158
|
}
|
|
143
|
-
for await (const line of readNdJsonStream(response.body,
|
|
159
|
+
for await (const { value: line, text } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
|
|
144
160
|
if (isStreamError(line)) {
|
|
145
161
|
throw new Error(`${line.payload.error}.`);
|
|
146
162
|
}
|
|
147
163
|
if (isStreamCloudEvent(line)) {
|
|
148
|
-
const event = convertCloudEventToEvent(line.payload);
|
|
164
|
+
const event = convertCloudEventToEvent(line.payload, getRawJsonMember(text, 'payload'));
|
|
149
165
|
yield event;
|
|
150
166
|
continue;
|
|
151
167
|
}
|
|
152
168
|
throw new Error('Failed to read events.');
|
|
153
169
|
}
|
|
170
|
+
stallGuard.throwIfStalled();
|
|
154
171
|
}
|
|
155
172
|
catch (error) {
|
|
173
|
+
stallGuard.throwIfStalled();
|
|
156
174
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
157
175
|
return;
|
|
158
176
|
}
|
|
159
177
|
throw error;
|
|
160
178
|
}
|
|
161
179
|
finally {
|
|
162
|
-
|
|
163
|
-
removeAbortListener();
|
|
164
|
-
}
|
|
165
|
-
if (shouldAbortInternally) {
|
|
166
|
-
internalAbortController.abort();
|
|
167
|
-
}
|
|
180
|
+
stallGuard.dispose({ abortRequest: true });
|
|
168
181
|
}
|
|
169
182
|
})();
|
|
170
183
|
}
|
|
171
184
|
runEventQlQuery(query, signal) {
|
|
172
185
|
const url = this.#getUrl('/api/v1/run-eventql-query');
|
|
173
186
|
const apiToken = this.#apiToken;
|
|
187
|
+
const stallTimeout = this.#stallTimeout;
|
|
174
188
|
return (async function* () {
|
|
175
|
-
const
|
|
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
|
-
}
|
|
189
|
+
const stallGuard = new StallGuard(stallTimeout, signal);
|
|
184
190
|
try {
|
|
185
|
-
|
|
191
|
+
stallGuard.restart();
|
|
192
|
+
const response = await stallGuard.send(url, {
|
|
186
193
|
method: 'post',
|
|
187
194
|
headers: {
|
|
188
195
|
authorization: `Bearer ${apiToken}`,
|
|
189
196
|
'content-type': 'application/json',
|
|
190
197
|
},
|
|
191
198
|
body: JSON.stringify({ query }),
|
|
192
|
-
signal: combinedSignal,
|
|
193
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));
|
|
194
203
|
if (!isValidServerHeader(response)) {
|
|
195
204
|
const serverHeader = response.headers.get('Server');
|
|
196
205
|
if (!serverHeader) {
|
|
@@ -204,7 +213,7 @@ class Client {
|
|
|
204
213
|
if (!response.body) {
|
|
205
214
|
throw new Error('Failed to run EventQL query.');
|
|
206
215
|
}
|
|
207
|
-
for await (const line of readNdJsonStream(response.body,
|
|
216
|
+
for await (const { value: line } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
|
|
208
217
|
if (isStreamHeartbeat(line)) {
|
|
209
218
|
continue;
|
|
210
219
|
}
|
|
@@ -218,38 +227,29 @@ class Client {
|
|
|
218
227
|
}
|
|
219
228
|
throw new Error('Failed to run EventQL query.');
|
|
220
229
|
}
|
|
230
|
+
stallGuard.throwIfStalled();
|
|
221
231
|
}
|
|
222
232
|
catch (error) {
|
|
233
|
+
stallGuard.throwIfStalled();
|
|
223
234
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
224
235
|
return;
|
|
225
236
|
}
|
|
226
237
|
throw error;
|
|
227
238
|
}
|
|
228
239
|
finally {
|
|
229
|
-
|
|
230
|
-
removeAbortListener();
|
|
231
|
-
}
|
|
232
|
-
if (shouldAbortInternally) {
|
|
233
|
-
internalAbortController.abort();
|
|
234
|
-
}
|
|
240
|
+
stallGuard.dispose({ abortRequest: true });
|
|
235
241
|
}
|
|
236
242
|
})();
|
|
237
243
|
}
|
|
238
244
|
observeEvents(subject, options, signal) {
|
|
239
245
|
const url = this.#getUrl('/api/v1/observe-events');
|
|
240
246
|
const apiToken = this.#apiToken;
|
|
247
|
+
const stallTimeout = this.#stallTimeout;
|
|
241
248
|
return (async function* () {
|
|
242
|
-
const
|
|
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
|
-
}
|
|
249
|
+
const stallGuard = new StallGuard(stallTimeout, signal);
|
|
251
250
|
try {
|
|
252
|
-
|
|
251
|
+
stallGuard.restart();
|
|
252
|
+
const response = await stallGuard.send(url, {
|
|
253
253
|
method: 'post',
|
|
254
254
|
headers: {
|
|
255
255
|
authorization: `Bearer ${apiToken}`,
|
|
@@ -259,8 +259,10 @@ class Client {
|
|
|
259
259
|
subject,
|
|
260
260
|
options,
|
|
261
261
|
}),
|
|
262
|
-
signal: combinedSignal,
|
|
263
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));
|
|
264
266
|
if (!isValidServerHeader(response)) {
|
|
265
267
|
const serverHeader = response.headers.get('Server');
|
|
266
268
|
if (!serverHeader) {
|
|
@@ -274,7 +276,7 @@ class Client {
|
|
|
274
276
|
if (!response.body) {
|
|
275
277
|
throw new Error('Failed to observe events.');
|
|
276
278
|
}
|
|
277
|
-
for await (const line of readNdJsonStream(response.body,
|
|
279
|
+
for await (const { value: line, text } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
|
|
278
280
|
if (isStreamHeartbeat(line)) {
|
|
279
281
|
continue;
|
|
280
282
|
}
|
|
@@ -282,64 +284,58 @@ class Client {
|
|
|
282
284
|
throw new Error(`${line.payload.error}.`);
|
|
283
285
|
}
|
|
284
286
|
if (isStreamCloudEvent(line)) {
|
|
285
|
-
const event = convertCloudEventToEvent(line.payload);
|
|
287
|
+
const event = convertCloudEventToEvent(line.payload, getRawJsonMember(text, 'payload'));
|
|
286
288
|
yield event;
|
|
287
289
|
continue;
|
|
288
290
|
}
|
|
289
291
|
throw new Error('Failed to observe events.');
|
|
290
292
|
}
|
|
293
|
+
stallGuard.throwIfStalled();
|
|
291
294
|
}
|
|
292
295
|
catch (error) {
|
|
296
|
+
stallGuard.throwIfStalled();
|
|
293
297
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
294
298
|
return;
|
|
295
299
|
}
|
|
296
300
|
throw error;
|
|
297
301
|
}
|
|
298
302
|
finally {
|
|
299
|
-
|
|
300
|
-
removeAbortListener();
|
|
301
|
-
}
|
|
302
|
-
if (shouldAbortInternally) {
|
|
303
|
-
internalAbortController.abort();
|
|
304
|
-
}
|
|
303
|
+
stallGuard.dispose({ abortRequest: true });
|
|
305
304
|
}
|
|
306
305
|
})();
|
|
307
306
|
}
|
|
308
|
-
async registerEventSchema(eventType, schema) {
|
|
307
|
+
async registerEventSchema(eventType, schema, signal) {
|
|
309
308
|
const url = this.#getUrl('/api/v1/register-event-schema');
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
328
|
});
|
|
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
|
-
}
|
|
327
329
|
}
|
|
328
330
|
readSubjects(baseSubject, signal) {
|
|
329
331
|
const url = this.#getUrl('/api/v1/read-subjects');
|
|
330
332
|
const apiToken = this.#apiToken;
|
|
333
|
+
const stallTimeout = this.#stallTimeout;
|
|
331
334
|
return (async function* () {
|
|
332
|
-
const
|
|
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
|
-
}
|
|
335
|
+
const stallGuard = new StallGuard(stallTimeout, signal);
|
|
341
336
|
try {
|
|
342
|
-
|
|
337
|
+
stallGuard.restart();
|
|
338
|
+
const response = await stallGuard.send(url, {
|
|
343
339
|
method: 'post',
|
|
344
340
|
headers: {
|
|
345
341
|
authorization: `Bearer ${apiToken}`,
|
|
@@ -348,7 +344,6 @@ class Client {
|
|
|
348
344
|
body: JSON.stringify({
|
|
349
345
|
baseSubject,
|
|
350
346
|
}),
|
|
351
|
-
signal: combinedSignal,
|
|
352
347
|
});
|
|
353
348
|
if (!isValidServerHeader(response)) {
|
|
354
349
|
const serverHeader = response.headers.get('Server');
|
|
@@ -363,7 +358,7 @@ class Client {
|
|
|
363
358
|
if (!response.body) {
|
|
364
359
|
throw new Error('Failed to read subjects.');
|
|
365
360
|
}
|
|
366
|
-
for await (const line of readNdJsonStream(response.body,
|
|
361
|
+
for await (const { value: line } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
|
|
367
362
|
if (isStreamError(line)) {
|
|
368
363
|
throw new Error(`${line.payload.error}.`);
|
|
369
364
|
}
|
|
@@ -373,70 +368,63 @@ class Client {
|
|
|
373
368
|
}
|
|
374
369
|
throw new Error('Failed to read subjects.');
|
|
375
370
|
}
|
|
371
|
+
stallGuard.throwIfStalled();
|
|
376
372
|
}
|
|
377
373
|
catch (error) {
|
|
374
|
+
stallGuard.throwIfStalled();
|
|
378
375
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
379
376
|
return;
|
|
380
377
|
}
|
|
381
378
|
throw error;
|
|
382
379
|
}
|
|
383
380
|
finally {
|
|
384
|
-
|
|
385
|
-
removeAbortListener();
|
|
386
|
-
}
|
|
387
|
-
if (shouldAbortInternally) {
|
|
388
|
-
internalAbortController.abort();
|
|
389
|
-
}
|
|
381
|
+
stallGuard.dispose({ abortRequest: true });
|
|
390
382
|
}
|
|
391
383
|
})();
|
|
392
384
|
}
|
|
393
|
-
async readEventType(eventType) {
|
|
385
|
+
async readEventType(eventType, signal) {
|
|
394
386
|
const url = this.#getUrl('/api/v1/read-event-type');
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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;
|
|
404
413
|
});
|
|
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;
|
|
419
414
|
}
|
|
420
415
|
readEventTypes(signal) {
|
|
421
416
|
const url = this.#getUrl('/api/v1/read-event-types');
|
|
422
417
|
const apiToken = this.#apiToken;
|
|
418
|
+
const stallTimeout = this.#stallTimeout;
|
|
423
419
|
return (async function* () {
|
|
424
|
-
const
|
|
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
|
-
}
|
|
420
|
+
const stallGuard = new StallGuard(stallTimeout, signal);
|
|
433
421
|
try {
|
|
434
|
-
|
|
422
|
+
stallGuard.restart();
|
|
423
|
+
const response = await stallGuard.send(url, {
|
|
435
424
|
method: 'post',
|
|
436
425
|
headers: {
|
|
437
426
|
authorization: `Bearer ${apiToken}`,
|
|
438
427
|
},
|
|
439
|
-
signal: combinedSignal,
|
|
440
428
|
});
|
|
441
429
|
if (!isValidServerHeader(response)) {
|
|
442
430
|
const serverHeader = response.headers.get('Server');
|
|
@@ -451,7 +439,7 @@ class Client {
|
|
|
451
439
|
if (!response.body) {
|
|
452
440
|
throw new Error('Failed to read event types.');
|
|
453
441
|
}
|
|
454
|
-
for await (const line of readNdJsonStream(response.body,
|
|
442
|
+
for await (const { value: line } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
|
|
455
443
|
if (isStreamHeartbeat(line)) {
|
|
456
444
|
continue;
|
|
457
445
|
}
|
|
@@ -464,20 +452,17 @@ class Client {
|
|
|
464
452
|
}
|
|
465
453
|
throw new Error('Failed to read event types.');
|
|
466
454
|
}
|
|
455
|
+
stallGuard.throwIfStalled();
|
|
467
456
|
}
|
|
468
457
|
catch (error) {
|
|
458
|
+
stallGuard.throwIfStalled();
|
|
469
459
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
470
460
|
return;
|
|
471
461
|
}
|
|
472
462
|
throw error;
|
|
473
463
|
}
|
|
474
464
|
finally {
|
|
475
|
-
|
|
476
|
-
removeAbortListener();
|
|
477
|
-
}
|
|
478
|
-
if (shouldAbortInternally) {
|
|
479
|
-
internalAbortController.abort();
|
|
480
|
-
}
|
|
465
|
+
stallGuard.dispose({ abortRequest: true });
|
|
481
466
|
}
|
|
482
467
|
})();
|
|
483
468
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 };
|
package/dist/Event.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ declare class Event {
|
|
|
14
14
|
traceparent?: string;
|
|
15
15
|
tracestate?: string;
|
|
16
16
|
signature: string | null;
|
|
17
|
-
constructor({ specversion, id, time, timeFromServer, source, subject, type, datacontenttype, data, hash, predecessorhash, traceparent, tracestate, signature, }: {
|
|
17
|
+
constructor({ specversion, id, time, timeFromServer, source, subject, type, datacontenttype, data, dataFromServer, hash, predecessorhash, traceparent, tracestate, signature, }: {
|
|
18
18
|
specversion: string;
|
|
19
19
|
id: string;
|
|
20
20
|
time: Date;
|
|
@@ -24,6 +24,7 @@ declare class Event {
|
|
|
24
24
|
type: string;
|
|
25
25
|
datacontenttype: string;
|
|
26
26
|
data: Record<string, unknown>;
|
|
27
|
+
dataFromServer: string;
|
|
27
28
|
hash: string;
|
|
28
29
|
predecessorhash: string;
|
|
29
30
|
traceparent?: string;
|