eventsourcingdb 1.8.10 → 1.9.1
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 +45 -0
- package/dist/Client.js +3 -2
- package/dist/HeartbeatTimeoutError.d.ts +4 -0
- package/dist/HeartbeatTimeoutError.js +7 -0
- package/dist/heartbeatTimeout.d.ts +4 -0
- package/dist/heartbeatTimeout.js +6 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/ndjson/readNdJsonStream.d.ts +1 -1
- package/dist/ndjson/readNdJsonStream.js +27 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -260,6 +260,29 @@ for await (const row of client.runEventQlQuery(`
|
|
|
260
260
|
controller.abort();
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
+
#### Detecting a Stalled Connection
|
|
264
|
+
|
|
265
|
+
While a query is running, EventSourcingDB sends a heartbeat every second whenever there is no row to send. If neither a row nor a heartbeat arrives for 30 seconds, for example because a proxy keeps the connection open but no longer passes anything through, the client SDK closes the connection and the iterator throws a `HeartbeatTimeoutError`:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { HeartbeatTimeoutError } from 'eventsourcingdb';
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
for await (const row of client.runEventQlQuery(`
|
|
272
|
+
FROM e IN events
|
|
273
|
+
PROJECT INTO e
|
|
274
|
+
`)) {
|
|
275
|
+
// ...
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error instanceof HeartbeatTimeoutError) {
|
|
279
|
+
// ...
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
*Note that the time you spend handling a row does not count towards the 30 seconds, and that aborting the query does not cause a `HeartbeatTimeoutError`.*
|
|
285
|
+
|
|
263
286
|
### Observing Events
|
|
264
287
|
|
|
265
288
|
To observe all events of a subject, call the `observeEvents` function with the subject as the first argument and an options object as the second argument. Set the `recursive` option to `false`. This ensures that only events of the given subject are returned, not events of nested subjects.
|
|
@@ -344,6 +367,28 @@ for await (const event of client.observeEvents('/books/42', {
|
|
|
344
367
|
controller.abort();
|
|
345
368
|
```
|
|
346
369
|
|
|
370
|
+
#### Detecting a Stalled Connection
|
|
371
|
+
|
|
372
|
+
While observing, EventSourcingDB sends a heartbeat every second whenever there is no event to send. If neither an event nor a heartbeat arrives for 30 seconds, for example because a proxy keeps the connection open but no longer passes anything through, the client SDK closes the connection and the iterator throws a `HeartbeatTimeoutError`:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { HeartbeatTimeoutError } from 'eventsourcingdb';
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
for await (const event of client.observeEvents('/books/42', {
|
|
379
|
+
recursive: false
|
|
380
|
+
})) {
|
|
381
|
+
// ...
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (error instanceof HeartbeatTimeoutError) {
|
|
385
|
+
// ...
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
*Note that the time you spend handling an event does not count towards the 30 seconds, and that aborting observing does not cause a `HeartbeatTimeoutError`.*
|
|
391
|
+
|
|
347
392
|
### Registering an Event Schema
|
|
348
393
|
|
|
349
394
|
To register an event schema, call the `registerEventSchema` function and hand over an event type and the desired schema:
|
package/dist/Client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { convertCloudEventToEvent } from './convertCloudEventToEvent.js';
|
|
2
|
+
import { heartbeatTimeout } from './heartbeatTimeout.js';
|
|
2
3
|
import { isCloudEvent } from './isCloudEvent.js';
|
|
3
4
|
import { isValidServerHeader } from './isValidServerHeader.js';
|
|
4
5
|
import { readNdJsonStream } from './ndjson/readNdJsonStream.js';
|
|
@@ -203,7 +204,7 @@ class Client {
|
|
|
203
204
|
if (!response.body) {
|
|
204
205
|
throw new Error('Failed to run EventQL query.');
|
|
205
206
|
}
|
|
206
|
-
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
207
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal, heartbeatTimeout.milliseconds)) {
|
|
207
208
|
if (isStreamHeartbeat(line)) {
|
|
208
209
|
continue;
|
|
209
210
|
}
|
|
@@ -273,7 +274,7 @@ class Client {
|
|
|
273
274
|
if (!response.body) {
|
|
274
275
|
throw new Error('Failed to observe events.');
|
|
275
276
|
}
|
|
276
|
-
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
277
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal, heartbeatTimeout.milliseconds)) {
|
|
277
278
|
if (isStreamHeartbeat(line)) {
|
|
278
279
|
continue;
|
|
279
280
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Container } from './Container.js';
|
|
|
3
3
|
import type { Event } from './Event.js';
|
|
4
4
|
import type { EventCandidate } from './EventCandidate.js';
|
|
5
5
|
import type { EventType } from './EventType.js';
|
|
6
|
+
import { HeartbeatTimeoutError } from './HeartbeatTimeoutError.js';
|
|
6
7
|
import { isEventQlQueryTrue } from './isEventQlQueryTrue.js';
|
|
7
8
|
import { isSubjectOnEventId } from './isSubjectOnEventId.js';
|
|
8
9
|
import { isSubjectPopulated } from './isSubjectPopulated.js';
|
|
@@ -11,4 +12,4 @@ import type { ObserveEventsOptions } from './ObserveEventsOptions.js';
|
|
|
11
12
|
import type { Precondition } from './Precondition.js';
|
|
12
13
|
import type { ReadEventsOptions } from './ReadEventsOptions.js';
|
|
13
14
|
export type { Event, EventCandidate, EventType, ObserveEventsOptions, Precondition, ReadEventsOptions, };
|
|
14
|
-
export { Client, Container, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, };
|
|
15
|
+
export { Client, Container, HeartbeatTimeoutError, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Client } from './Client.js';
|
|
2
2
|
import { Container } from './Container.js';
|
|
3
|
+
import { HeartbeatTimeoutError } from './HeartbeatTimeoutError.js';
|
|
3
4
|
import { isEventQlQueryTrue } from './isEventQlQueryTrue.js';
|
|
4
5
|
import { isSubjectOnEventId } from './isSubjectOnEventId.js';
|
|
5
6
|
import { isSubjectPopulated } from './isSubjectPopulated.js';
|
|
6
7
|
import { isSubjectPristine } from './isSubjectPristine.js';
|
|
7
|
-
export { Client, Container, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, };
|
|
8
|
+
export { Client, Container, HeartbeatTimeoutError, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const readNdJsonStream: (stream: ReadableStream<Uint8Array>, signal: AbortSignal) => AsyncGenerator<Record<string, unknown>, void, void>;
|
|
1
|
+
declare const readNdJsonStream: (stream: ReadableStream<Uint8Array>, signal: AbortSignal, heartbeatTimeoutInMilliseconds?: number) => AsyncGenerator<Record<string, unknown>, void, void>;
|
|
2
2
|
export { readNdJsonStream };
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { HeartbeatTimeoutError } from '../HeartbeatTimeoutError.js';
|
|
2
|
+
const readNdJsonStream = async function* (stream, signal, heartbeatTimeoutInMilliseconds) {
|
|
2
3
|
const reader = stream.getReader();
|
|
3
4
|
const decoder = new TextDecoder('utf-8');
|
|
4
5
|
let buffer = '';
|
|
6
|
+
let heartbeatTimer;
|
|
7
|
+
let hasHeartbeatTimedOut = false;
|
|
8
|
+
// The timer only runs while waiting for the next line, not while the caller
|
|
9
|
+
// handles a line, so a slow caller does not cause a heartbeat timeout.
|
|
10
|
+
const startHeartbeatTimer = () => {
|
|
11
|
+
if (heartbeatTimeoutInMilliseconds === undefined || heartbeatTimer !== undefined) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
heartbeatTimer = setTimeout(() => {
|
|
15
|
+
hasHeartbeatTimedOut = true;
|
|
16
|
+
reader.cancel().catch(() => {
|
|
17
|
+
// Intentionally left blank.
|
|
18
|
+
});
|
|
19
|
+
}, heartbeatTimeoutInMilliseconds);
|
|
20
|
+
};
|
|
21
|
+
const stopHeartbeatTimer = () => {
|
|
22
|
+
clearTimeout(heartbeatTimer);
|
|
23
|
+
heartbeatTimer = undefined;
|
|
24
|
+
};
|
|
5
25
|
const onAbort = () => {
|
|
6
26
|
reader.cancel().catch(() => {
|
|
7
27
|
// Intentionally left blank.
|
|
@@ -16,6 +36,7 @@ const readNdJsonStream = async function* (stream, signal) {
|
|
|
16
36
|
signal.addEventListener('abort', onAbort);
|
|
17
37
|
try {
|
|
18
38
|
while (!signal.aborted) {
|
|
39
|
+
startHeartbeatTimer();
|
|
19
40
|
// biome-ignore lint/performance/noAwaitInLoops: Awaiting the result is fine here, although we are in a loop.
|
|
20
41
|
const { done, value } = await reader.read();
|
|
21
42
|
if (done) {
|
|
@@ -27,13 +48,18 @@ const readNdJsonStream = async function* (stream, signal) {
|
|
|
27
48
|
const line = buffer.slice(0, index).trim();
|
|
28
49
|
buffer = buffer.slice(index + 1);
|
|
29
50
|
if (line) {
|
|
51
|
+
stopHeartbeatTimer();
|
|
30
52
|
yield JSON.parse(line);
|
|
31
53
|
}
|
|
32
54
|
index = buffer.indexOf('\n');
|
|
33
55
|
}
|
|
34
56
|
}
|
|
57
|
+
if (hasHeartbeatTimedOut) {
|
|
58
|
+
throw new HeartbeatTimeoutError();
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
61
|
finally {
|
|
62
|
+
stopHeartbeatTimer();
|
|
37
63
|
signal.removeEventListener('abort', onAbort);
|
|
38
64
|
await reader.cancel().catch(() => {
|
|
39
65
|
// Intentionally left blank.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eventsourcingdb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "The official JavaScript client SDK for EventSourcingDB.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "the native web GmbH",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"testcontainers": "12.
|
|
21
|
+
"testcontainers": "12.2.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@biomejs/biome": "2.5.14",
|