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
@@ -0,0 +1,46 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import diagnosticsChannel from 'node:diagnostics_channel';
3
+ const currentSending = new AsyncLocalStorage();
4
+ const sendingByRequest = new WeakMap();
5
+ const getRequest = (message) => {
6
+ if (typeof message !== 'object' || message === null || !('request' in message)) {
7
+ return;
8
+ }
9
+ const { request } = message;
10
+ if (typeof request !== 'object' || request === null) {
11
+ return;
12
+ }
13
+ return request;
14
+ };
15
+ // The fetch built into Node.js creates its request within the call of fetch,
16
+ // so the request belongs to the write that runs there.
17
+ diagnosticsChannel.subscribe('undici:request:create', (message) => {
18
+ const sending = currentSending.getStore();
19
+ const request = getRequest(message);
20
+ if (sending === undefined || request === undefined || sending.state !== 'unknown') {
21
+ return;
22
+ }
23
+ sending.state = 'created';
24
+ sendingByRequest.set(request, sending);
25
+ });
26
+ diagnosticsChannel.subscribe('undici:client:sendHeaders', (message) => {
27
+ const request = getRequest(message);
28
+ const sending = request === undefined ? undefined : sendingByRequest.get(request);
29
+ if (sending !== undefined) {
30
+ sending.state = 'sent';
31
+ }
32
+ });
33
+ // Runs a write and tells, if it fails, whether its request certainly did not
34
+ // reach the server, since it was created but its headers never went out. If
35
+ // the client cannot tell, for example since fetch is not the one built into
36
+ // Node.js, the request counts as sent.
37
+ const trackSending = async (write) => {
38
+ const sending = { state: 'unknown' };
39
+ try {
40
+ return { result: await currentSending.run(sending, write) };
41
+ }
42
+ catch (error) {
43
+ return { error, hasBeenSent: sending.state !== 'created' };
44
+ }
45
+ };
46
+ export { trackSending };
@@ -0,0 +1,2 @@
1
+ declare const waitUnlessAborted: <TResult>(promise: Promise<TResult>, signal?: AbortSignal) => Promise<TResult>;
2
+ export { waitUnlessAborted };
@@ -0,0 +1,21 @@
1
+ // Waits for a promise that other callers may share, unless the signal of this
2
+ // caller aborts first, which then rejects with its reason, while the promise
3
+ // goes on for the others.
4
+ const waitUnlessAborted = async (promise, signal) => {
5
+ if (signal === undefined) {
6
+ return await promise;
7
+ }
8
+ signal.throwIfAborted();
9
+ const aborted = Promise.withResolvers();
10
+ const onAbort = () => {
11
+ aborted.reject(signal.reason);
12
+ };
13
+ signal.addEventListener('abort', onAbort, { once: true });
14
+ try {
15
+ return await Promise.race([promise, aborted.promise]);
16
+ }
17
+ finally {
18
+ signal.removeEventListener('abort', onAbort);
19
+ }
20
+ };
21
+ export { waitUnlessAborted };
package/dist/index.d.ts CHANGED
@@ -1,15 +1,24 @@
1
1
  import { Client } from './Client.js';
2
+ import type { ClientOptions } from './ClientOptions.js';
3
+ import { ConnectionStalledError } from './ConnectionStalledError.js';
2
4
  import { Container } from './Container.js';
5
+ import type { DbApiErrorOptions } from './DbApiError.js';
6
+ import { DbApiError } from './DbApiError.js';
7
+ import { ErrorCode } from './ErrorCode.js';
3
8
  import type { Event } from './Event.js';
4
9
  import type { EventCandidate } from './EventCandidate.js';
5
10
  import type { EventType } from './EventType.js';
6
- import { HeartbeatTimeoutError } from './HeartbeatTimeoutError.js';
11
+ import { HashVerificationFailedError } from './HashVerificationFailedError.js';
7
12
  import { isEventQlQueryTrue } from './isEventQlQueryTrue.js';
8
13
  import { isSubjectOnEventId } from './isSubjectOnEventId.js';
9
14
  import { isSubjectPopulated } from './isSubjectPopulated.js';
10
15
  import { isSubjectPristine } from './isSubjectPristine.js';
11
16
  import type { ObserveEventsOptions } from './ObserveEventsOptions.js';
17
+ import { OutcomeUnknownError } from './OutcomeUnknownError.js';
12
18
  import type { Precondition } from './Precondition.js';
13
19
  import type { ReadEventsOptions } from './ReadEventsOptions.js';
14
- export type { Event, EventCandidate, EventType, ObserveEventsOptions, Precondition, ReadEventsOptions, };
15
- export { Client, Container, HeartbeatTimeoutError, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, };
20
+ import type { ReadSubjectsOptions } from './ReadSubjectsOptions.js';
21
+ import { SignatureVerificationFailedError } from './SignatureVerificationFailedError.js';
22
+ import { UnsupportedHashVersionError } from './UnsupportedHashVersionError.js';
23
+ export type { ClientOptions, DbApiErrorOptions, Event, EventCandidate, EventType, ObserveEventsOptions, Precondition, ReadEventsOptions, ReadSubjectsOptions, };
24
+ export { Client, ConnectionStalledError, Container, DbApiError, ErrorCode, HashVerificationFailedError, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, OutcomeUnknownError, SignatureVerificationFailedError, UnsupportedHashVersionError, };
package/dist/index.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import { Client } from './Client.js';
2
+ import { ConnectionStalledError } from './ConnectionStalledError.js';
2
3
  import { Container } from './Container.js';
3
- import { HeartbeatTimeoutError } from './HeartbeatTimeoutError.js';
4
+ import { DbApiError } from './DbApiError.js';
5
+ import { ErrorCode } from './ErrorCode.js';
6
+ import { HashVerificationFailedError } from './HashVerificationFailedError.js';
4
7
  import { isEventQlQueryTrue } from './isEventQlQueryTrue.js';
5
8
  import { isSubjectOnEventId } from './isSubjectOnEventId.js';
6
9
  import { isSubjectPopulated } from './isSubjectPopulated.js';
7
10
  import { isSubjectPristine } from './isSubjectPristine.js';
8
- export { Client, Container, HeartbeatTimeoutError, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, };
11
+ import { OutcomeUnknownError } from './OutcomeUnknownError.js';
12
+ import { SignatureVerificationFailedError } from './SignatureVerificationFailedError.js';
13
+ import { UnsupportedHashVersionError } from './UnsupportedHashVersionError.js';
14
+ export { Client, ConnectionStalledError, Container, DbApiError, ErrorCode, HashVerificationFailedError, isEventQlQueryTrue, isSubjectOnEventId, isSubjectPopulated, isSubjectPristine, OutcomeUnknownError, SignatureVerificationFailedError, UnsupportedHashVersionError, };
@@ -0,0 +1,2 @@
1
+ declare const getRawJsonElements: (text: string) => string[];
2
+ export { getRawJsonElements };
@@ -0,0 +1,22 @@
1
+ import { scanJsonValue, skipJsonWhitespace } from './scanJson.js';
2
+ // Returns the elements of a JSON array exactly as the text contains them, or
3
+ // an empty array if the text is not an array.
4
+ const getRawJsonElements = (text) => {
5
+ const elements = [];
6
+ let position = skipJsonWhitespace(text, 0);
7
+ if (text[position] !== '[') {
8
+ return elements;
9
+ }
10
+ position = skipJsonWhitespace(text, position + 1);
11
+ while (position < text.length && text[position] !== ']') {
12
+ const elementEnd = scanJsonValue(text, position);
13
+ elements.push(text.slice(position, elementEnd));
14
+ position = skipJsonWhitespace(text, elementEnd);
15
+ if (text[position] !== ',') {
16
+ break;
17
+ }
18
+ position = skipJsonWhitespace(text, position + 1);
19
+ }
20
+ return elements;
21
+ };
22
+ export { getRawJsonElements };
@@ -0,0 +1,2 @@
1
+ declare const getRawJsonMember: (text: string, key: string) => string | undefined;
2
+ export { getRawJsonMember };
@@ -0,0 +1,29 @@
1
+ import { scanJsonString, scanJsonValue, skipJsonWhitespace } from './scanJson.js';
2
+ // Returns the value of a member of a JSON object exactly as the text contains
3
+ // it, or undefined if the text is not an object or has no such member. If the
4
+ // object has the member more than once, the last one wins, as with JSON.parse.
5
+ const getRawJsonMember = (text, key) => {
6
+ let position = skipJsonWhitespace(text, 0);
7
+ if (text[position] !== '{') {
8
+ return;
9
+ }
10
+ position = skipJsonWhitespace(text, position + 1);
11
+ let rawValue;
12
+ while (text[position] === '"') {
13
+ const keyEnd = scanJsonString(text, position);
14
+ const currentKey = JSON.parse(text.slice(position, keyEnd));
15
+ const colon = skipJsonWhitespace(text, keyEnd);
16
+ const valueStart = skipJsonWhitespace(text, colon + 1);
17
+ const valueEnd = scanJsonValue(text, valueStart);
18
+ if (currentKey === key) {
19
+ rawValue = text.slice(valueStart, valueEnd);
20
+ }
21
+ position = skipJsonWhitespace(text, valueEnd);
22
+ if (text[position] !== ',') {
23
+ break;
24
+ }
25
+ position = skipJsonWhitespace(text, position + 1);
26
+ }
27
+ return rawValue;
28
+ };
29
+ export { getRawJsonMember };
@@ -0,0 +1,4 @@
1
+ declare const skipJsonWhitespace: (text: string, start: number) => number;
2
+ declare const scanJsonString: (text: string, start: number) => number;
3
+ declare const scanJsonValue: (text: string, start: number) => number;
4
+ export { scanJsonString, scanJsonValue, skipJsonWhitespace };
@@ -0,0 +1,65 @@
1
+ // These functions find where a value starts and ends in a JSON text, so that
2
+ // the SDK can take a value exactly as the server sent it, byte for byte. They
3
+ // expect valid JSON, since the SDK only scans texts that JSON.parse accepted.
4
+ const isJsonWhitespace = (character) => character === ' ' || character === '\t' || character === '\n' || character === '\r';
5
+ const isJsonDelimiter = (character) => character === ',' || character === '}' || character === ']' || isJsonWhitespace(character);
6
+ const skipJsonWhitespace = (text, start) => {
7
+ let position = start;
8
+ while (isJsonWhitespace(text[position])) {
9
+ position += 1;
10
+ }
11
+ return position;
12
+ };
13
+ const scanJsonString = (text, start) => {
14
+ let position = start + 1;
15
+ while (position < text.length) {
16
+ const character = text[position];
17
+ if (character === '\\') {
18
+ position += 2;
19
+ continue;
20
+ }
21
+ if (character === '"') {
22
+ return position + 1;
23
+ }
24
+ position += 1;
25
+ }
26
+ throw new Error('Failed to scan JSON string.');
27
+ };
28
+ const scanJsonContainer = (text, start) => {
29
+ let depth = 0;
30
+ let position = start;
31
+ while (position < text.length) {
32
+ const character = text[position];
33
+ if (character === '"') {
34
+ position = scanJsonString(text, position);
35
+ continue;
36
+ }
37
+ if (character === '{' || character === '[') {
38
+ depth += 1;
39
+ }
40
+ if (character === '}' || character === ']') {
41
+ depth -= 1;
42
+ if (depth === 0) {
43
+ return position + 1;
44
+ }
45
+ }
46
+ position += 1;
47
+ }
48
+ throw new Error('Failed to scan JSON container.');
49
+ };
50
+ // Returns the position right after the value that starts at the given position.
51
+ const scanJsonValue = (text, start) => {
52
+ const character = text[start];
53
+ if (character === '"') {
54
+ return scanJsonString(text, start);
55
+ }
56
+ if (character === '{' || character === '[') {
57
+ return scanJsonContainer(text, start);
58
+ }
59
+ let position = start;
60
+ while (position < text.length && !isJsonDelimiter(text[position])) {
61
+ position += 1;
62
+ }
63
+ return position;
64
+ };
65
+ export { scanJsonString, scanJsonValue, skipJsonWhitespace };
@@ -1,2 +1,8 @@
1
- declare const readNdJsonStream: (stream: ReadableStream<Uint8Array>, signal: AbortSignal, heartbeatTimeoutInMilliseconds?: number) => AsyncGenerator<Record<string, unknown>, void, void>;
1
+ import type { StallGuard } from '../stall/StallGuard.js';
2
+ interface NdJsonLine {
3
+ text: string;
4
+ value: Record<string, unknown>;
5
+ }
6
+ declare const readNdJsonStream: (stream: ReadableStream<Uint8Array>, signal: AbortSignal, stallGuard?: StallGuard) => AsyncGenerator<NdJsonLine, void, void>;
7
+ export type { NdJsonLine };
2
8
  export { readNdJsonStream };
@@ -1,27 +1,7 @@
1
- import { HeartbeatTimeoutError } from '../HeartbeatTimeoutError.js';
2
- const readNdJsonStream = async function* (stream, signal, heartbeatTimeoutInMilliseconds) {
1
+ const readNdJsonStream = async function* (stream, signal, stallGuard) {
3
2
  const reader = stream.getReader();
4
3
  const decoder = new TextDecoder('utf-8');
5
4
  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
- };
25
5
  const onAbort = () => {
26
6
  reader.cancel().catch(() => {
27
7
  // Intentionally left blank.
@@ -36,7 +16,9 @@ const readNdJsonStream = async function* (stream, signal, heartbeatTimeoutInMill
36
16
  signal.addEventListener('abort', onAbort);
37
17
  try {
38
18
  while (!signal.aborted) {
39
- startHeartbeatTimer();
19
+ // The clock only runs while waiting for the next chunk, not while the
20
+ // caller handles a line, so a slow caller does not cause a stall.
21
+ stallGuard?.restart();
40
22
  // biome-ignore lint/performance/noAwaitInLoops: Awaiting the result is fine here, although we are in a loop.
41
23
  const { done, value } = await reader.read();
42
24
  if (done) {
@@ -48,18 +30,15 @@ const readNdJsonStream = async function* (stream, signal, heartbeatTimeoutInMill
48
30
  const line = buffer.slice(0, index).trim();
49
31
  buffer = buffer.slice(index + 1);
50
32
  if (line) {
51
- stopHeartbeatTimer();
52
- yield JSON.parse(line);
33
+ stallGuard?.pause();
34
+ yield { text: line, value: JSON.parse(line) };
53
35
  }
54
36
  index = buffer.indexOf('\n');
55
37
  }
56
38
  }
57
- if (hasHeartbeatTimedOut) {
58
- throw new HeartbeatTimeoutError();
59
- }
60
39
  }
61
40
  finally {
62
- stopHeartbeatTimer();
41
+ stallGuard?.pause();
63
42
  signal.removeEventListener('abort', onAbort);
64
43
  await reader.cancel().catch(() => {
65
44
  // Intentionally left blank.
@@ -0,0 +1,13 @@
1
+ declare class StallGuard {
2
+ #private;
3
+ constructor(stallTimeout: number, callerSignal?: AbortSignal);
4
+ get signal(): AbortSignal;
5
+ send(url: string, init: RequestInit): Promise<Response>;
6
+ restart(): void;
7
+ pause(): void;
8
+ throwIfStalled(): void;
9
+ dispose({ abortRequest }: {
10
+ abortRequest: boolean;
11
+ }): void;
12
+ }
13
+ export { StallGuard };
@@ -0,0 +1,77 @@
1
+ import { ConnectionStalledError } from '../ConnectionStalledError.js';
2
+ import { createFetchTimeoutError } from './createFetchTimeoutError.js';
3
+ // Ends a request once nothing arrives from the server for the stall timeout,
4
+ // from sending the request until its end, including connecting and waiting
5
+ // for the response headers. Everything that arrives restarts the clock, and
6
+ // the clock stops while the caller handles an item of a stream. Aborting the
7
+ // signal of the caller aborts the request as well.
8
+ class StallGuard {
9
+ #stallTimeout;
10
+ #controller = new AbortController();
11
+ #callerSignal;
12
+ #phase = 'headers';
13
+ #stallError;
14
+ #timer;
15
+ #onCallerAbort = () => {
16
+ this.#controller.abort(this.#callerSignal?.reason);
17
+ };
18
+ constructor(stallTimeout, callerSignal) {
19
+ this.#stallTimeout = stallTimeout;
20
+ this.#callerSignal = callerSignal;
21
+ if (callerSignal?.aborted) {
22
+ this.#onCallerAbort();
23
+ }
24
+ else {
25
+ callerSignal?.addEventListener('abort', this.#onCallerAbort, { once: true });
26
+ }
27
+ }
28
+ // The signal of the request, which aborts once the request stalls or the
29
+ // caller aborts.
30
+ get signal() {
31
+ return this.#controller.signal;
32
+ }
33
+ // Sends the request with the signal of the guard. Once the headers of the
34
+ // response have arrived, the clock starts anew.
35
+ async send(url, init) {
36
+ const response = await fetch(url, { ...init, signal: this.#controller.signal });
37
+ this.#phase = 'body';
38
+ this.restart();
39
+ return response;
40
+ }
41
+ // Starts the clock, or starts it anew, since something arrived.
42
+ restart() {
43
+ if (this.#controller.signal.aborted) {
44
+ return;
45
+ }
46
+ // Starts a new timer rather than refreshing the running one, since the
47
+ // mocked timers of node:test do not move a refreshed timer.
48
+ this.pause();
49
+ this.#timer = setTimeout(() => {
50
+ this.#timer = undefined;
51
+ this.#stallError = new ConnectionStalledError(this.#stallTimeout, {
52
+ cause: createFetchTimeoutError(this.#phase),
53
+ });
54
+ this.#controller.abort(this.#stallError);
55
+ }, this.#stallTimeout);
56
+ }
57
+ // Stops the clock, while the caller handles an item of a stream.
58
+ pause() {
59
+ clearTimeout(this.#timer);
60
+ this.#timer = undefined;
61
+ }
62
+ throwIfStalled() {
63
+ if (this.#stallError !== undefined) {
64
+ throw this.#stallError;
65
+ }
66
+ }
67
+ // Stops watching the request. Aborting it as well closes its connection,
68
+ // which a stream needs, since it may end before its body does.
69
+ dispose({ abortRequest }) {
70
+ this.pause();
71
+ this.#callerSignal?.removeEventListener('abort', this.#onCallerAbort);
72
+ if (abortRequest) {
73
+ this.#controller.abort();
74
+ }
75
+ }
76
+ }
77
+ export { StallGuard };
@@ -0,0 +1,2 @@
1
+ declare const createFetchTimeoutError: (phase: 'headers' | 'body') => Error;
2
+ export { createFetchTimeoutError };
@@ -0,0 +1,16 @@
1
+ // Creates an error like the one that the fetch built into Node.js gives as
2
+ // the cause of its TypeError when it times out, with the same name, code, and
3
+ // message, so that code that checks the code of the cause keeps working.
4
+ const createFetchTimeoutError = (phase) => {
5
+ if (phase === 'headers') {
6
+ return Object.assign(new Error('Headers Timeout Error'), {
7
+ name: 'HeadersTimeoutError',
8
+ code: 'UND_ERR_HEADERS_TIMEOUT',
9
+ });
10
+ }
11
+ return Object.assign(new Error('Body Timeout Error'), {
12
+ name: 'BodyTimeoutError',
13
+ code: 'UND_ERR_BODY_TIMEOUT',
14
+ });
15
+ };
16
+ export { createFetchTimeoutError };
@@ -0,0 +1,3 @@
1
+ declare const defaultStallTimeout = 30000;
2
+ declare const maximumStallTimeout = 2147483647;
3
+ export { defaultStallTimeout, maximumStallTimeout };
@@ -0,0 +1,6 @@
1
+ // A request fails once nothing arrives from the server for this many
2
+ // milliseconds, unless the client is configured otherwise.
3
+ const defaultStallTimeout = 30_000;
4
+ // Node.js runs a timer for at most this many milliseconds.
5
+ const maximumStallTimeout = 2_147_483_647;
6
+ export { defaultStallTimeout, maximumStallTimeout };
@@ -0,0 +1,2 @@
1
+ declare const formatDuration: (milliseconds: number) => string;
2
+ export { formatDuration };
@@ -0,0 +1,9 @@
1
+ const millisecondsPerSecond = 1000;
2
+ const formatDuration = (milliseconds) => {
3
+ if (milliseconds % millisecondsPerSecond === 0) {
4
+ const seconds = milliseconds / millisecondsPerSecond;
5
+ return `${seconds} ${seconds === 1 ? 'second' : 'seconds'}`;
6
+ }
7
+ return `${milliseconds} ${milliseconds === 1 ? 'millisecond' : 'milliseconds'}`;
8
+ };
9
+ export { formatDuration };
@@ -0,0 +1,3 @@
1
+ import type { StallGuard } from './StallGuard.js';
2
+ declare const readResponseText: (response: Response, stallGuard: StallGuard) => Promise<string>;
3
+ export { readResponseText };
@@ -0,0 +1,26 @@
1
+ // Reads the body of a response as text, like response.text(), but restarts
2
+ // the clock of the stall guard with every chunk that arrives.
3
+ const readResponseText = async (response, stallGuard) => {
4
+ if (!response.body) {
5
+ return '';
6
+ }
7
+ const reader = response.body.getReader();
8
+ const decoder = new TextDecoder('utf-8');
9
+ let text = '';
10
+ try {
11
+ stallGuard.restart();
12
+ let result = await reader.read();
13
+ while (!result.done) {
14
+ text += decoder.decode(result.value, { stream: true });
15
+ stallGuard.restart();
16
+ // biome-ignore lint/performance/noAwaitInLoops: Awaiting the result is fine here, although we are in a loop.
17
+ result = await reader.read();
18
+ }
19
+ text += decoder.decode();
20
+ }
21
+ finally {
22
+ reader.releaseLock();
23
+ }
24
+ return text;
25
+ };
26
+ export { readResponseText };
@@ -0,0 +1,6 @@
1
+ import { StallGuard } from './StallGuard.js';
2
+ declare const runWithStallGuard: <TResult>({ stallTimeout, signal }: {
3
+ stallTimeout: number;
4
+ signal?: AbortSignal;
5
+ }, run: (stallGuard: StallGuard) => Promise<TResult>) => Promise<TResult>;
6
+ export { runWithStallGuard };
@@ -0,0 +1,18 @@
1
+ import { StallGuard } from './StallGuard.js';
2
+ // Runs a request that is not a stream under a stall guard. If the request
3
+ // stalls, it fails with the error of the stall, whatever fetch throws then.
4
+ const runWithStallGuard = async ({ stallTimeout, signal }, run) => {
5
+ const stallGuard = new StallGuard(stallTimeout, signal);
6
+ try {
7
+ stallGuard.restart();
8
+ return await run(stallGuard);
9
+ }
10
+ catch (error) {
11
+ stallGuard.throwIfStalled();
12
+ throw error;
13
+ }
14
+ finally {
15
+ stallGuard.dispose({ abortRequest: false });
16
+ }
17
+ };
18
+ export { runWithStallGuard };
@@ -0,0 +1,6 @@
1
+ import { StallGuard } from './StallGuard.js';
2
+ declare const streamWithStallGuard: <TItem>({ stallTimeout, signal }: {
3
+ stallTimeout: number;
4
+ signal?: AbortSignal;
5
+ }, stream: (stallGuard: StallGuard) => AsyncGenerator<TItem, void, void>) => AsyncGenerator<TItem, void, void>;
6
+ export { streamWithStallGuard };
@@ -0,0 +1,25 @@
1
+ import { isAbortError } from '../http/isAbortError.js';
2
+ import { StallGuard } from './StallGuard.js';
3
+ // Runs a request that is a stream under a stall guard. If the request stalls,
4
+ // the stream fails with the error of the stall, whatever fetch throws then. If
5
+ // the caller aborts, the stream ends without an error. Ending the stream early
6
+ // closes the connection of the request.
7
+ const streamWithStallGuard = async function* ({ stallTimeout, signal }, stream) {
8
+ const stallGuard = new StallGuard(stallTimeout, signal);
9
+ try {
10
+ stallGuard.restart();
11
+ yield* stream(stallGuard);
12
+ stallGuard.throwIfStalled();
13
+ }
14
+ catch (error) {
15
+ stallGuard.throwIfStalled();
16
+ if (isAbortError(error)) {
17
+ return;
18
+ }
19
+ throw error;
20
+ }
21
+ finally {
22
+ stallGuard.dispose({ abortRequest: true });
23
+ }
24
+ };
25
+ export { streamWithStallGuard };
@@ -0,0 +1,23 @@
1
+ import type { Event } from '../Event.js';
2
+ import type { EventCandidate } from '../EventCandidate.js';
3
+ import type { EventType } from '../EventType.js';
4
+ import type { ObserveEventsOptions } from '../ObserveEventsOptions.js';
5
+ import type { Precondition } from '../Precondition.js';
6
+ import type { ReadEventsOptions } from '../ReadEventsOptions.js';
7
+ import type { ReadSubjectsOptions } from '../ReadSubjectsOptions.js';
8
+ import type { Transport, TransportOptions } from '../Transport.js';
9
+ declare class V1Transport implements Transport {
10
+ #private;
11
+ constructor({ url, apiToken, stallTimeout }: TransportOptions);
12
+ ping(signal?: AbortSignal): Promise<void>;
13
+ verifyApiToken(signal?: AbortSignal): Promise<void>;
14
+ writeEvents(events: EventCandidate[], preconditions: Precondition[], signal?: AbortSignal): Promise<Event[]>;
15
+ readEvents(subject: string, options: ReadEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
16
+ runEventQlQuery(query: string, signal?: AbortSignal): AsyncGenerator<unknown, void, void>;
17
+ observeEvents(subject: string, options: ObserveEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
18
+ registerEventSchema(eventType: string, schema: Record<string, unknown>, signal?: AbortSignal): Promise<void>;
19
+ readSubjects(subject: string, { recursive }: ReadSubjectsOptions, signal?: AbortSignal): AsyncGenerator<string, void, void>;
20
+ readEventType(eventType: string, signal?: AbortSignal): Promise<EventType>;
21
+ readEventTypes(signal?: AbortSignal): AsyncGenerator<EventType, void, void>;
22
+ }
23
+ export { V1Transport };