@s2-dev/streamstore 0.19.5 → 0.21.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/LICENSE +21 -201
- package/README.md +60 -10
- package/dist/cjs/accessTokens.d.ts +27 -14
- package/dist/cjs/accessTokens.d.ts.map +1 -1
- package/dist/cjs/accessTokens.js +72 -8
- package/dist/cjs/accessTokens.js.map +1 -1
- package/dist/cjs/basins.d.ts +29 -19
- package/dist/cjs/basins.d.ts.map +1 -1
- package/dist/cjs/basins.js +119 -9
- package/dist/cjs/basins.js.map +1 -1
- package/dist/cjs/batch-transform.d.ts +12 -16
- package/dist/cjs/batch-transform.d.ts.map +1 -1
- package/dist/cjs/batch-transform.js +17 -21
- package/dist/cjs/batch-transform.js.map +1 -1
- package/dist/cjs/common.d.ts +31 -24
- package/dist/cjs/common.d.ts.map +1 -1
- package/dist/cjs/common.js +22 -0
- package/dist/cjs/common.js.map +1 -1
- package/dist/cjs/endpoints.d.ts +63 -0
- package/dist/cjs/endpoints.d.ts.map +1 -0
- package/dist/cjs/endpoints.js +120 -0
- package/dist/cjs/endpoints.js.map +1 -0
- package/dist/cjs/error.d.ts.map +1 -1
- package/dist/cjs/error.js +11 -0
- package/dist/cjs/error.js.map +1 -1
- package/dist/cjs/generated/types.gen.d.ts +11 -20
- package/dist/cjs/generated/types.gen.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +30 -46
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +50 -26
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internal/case-transform.d.ts +59 -0
- package/dist/cjs/internal/case-transform.d.ts.map +1 -0
- package/dist/cjs/internal/case-transform.js +80 -0
- package/dist/cjs/internal/case-transform.js.map +1 -0
- package/dist/cjs/internal/mappers.d.ts +51 -0
- package/dist/cjs/internal/mappers.d.ts.map +1 -0
- package/dist/cjs/internal/mappers.js +225 -0
- package/dist/cjs/internal/mappers.js.map +1 -0
- package/dist/cjs/internal/sdk-types.d.ts +127 -0
- package/dist/cjs/internal/sdk-types.d.ts.map +1 -0
- package/dist/cjs/internal/sdk-types.js +9 -0
- package/dist/cjs/internal/sdk-types.js.map +1 -0
- package/dist/cjs/lib/base64.d.ts +8 -0
- package/dist/cjs/lib/base64.d.ts.map +1 -1
- package/dist/cjs/lib/base64.js +32 -12
- package/dist/cjs/lib/base64.js.map +1 -1
- package/dist/cjs/lib/event-stream.d.ts.map +1 -1
- package/dist/cjs/lib/event-stream.js +2 -1
- package/dist/cjs/lib/event-stream.js.map +1 -1
- package/dist/cjs/lib/paginate.d.ts +57 -0
- package/dist/cjs/lib/paginate.d.ts.map +1 -0
- package/dist/cjs/lib/paginate.js +51 -0
- package/dist/cjs/lib/paginate.js.map +1 -0
- package/dist/cjs/lib/result.d.ts +1 -1
- package/dist/cjs/lib/result.d.ts.map +1 -1
- package/dist/cjs/lib/retry.d.ts +47 -31
- package/dist/cjs/lib/retry.d.ts.map +1 -1
- package/dist/cjs/lib/retry.js +302 -201
- package/dist/cjs/lib/retry.js.map +1 -1
- package/dist/cjs/lib/stream/runtime.d.ts +1 -1
- package/dist/cjs/lib/stream/transport/fetch/index.d.ts +7 -9
- package/dist/cjs/lib/stream/transport/fetch/index.d.ts.map +1 -1
- package/dist/cjs/lib/stream/transport/fetch/index.js +38 -39
- package/dist/cjs/lib/stream/transport/fetch/index.js.map +1 -1
- package/dist/cjs/lib/stream/transport/fetch/shared.d.ts +7 -2
- package/dist/cjs/lib/stream/transport/fetch/shared.d.ts.map +1 -1
- package/dist/cjs/lib/stream/transport/fetch/shared.js +56 -110
- package/dist/cjs/lib/stream/transport/fetch/shared.js.map +1 -1
- package/dist/cjs/lib/stream/transport/proto.d.ts +9 -0
- package/dist/cjs/lib/stream/transport/proto.d.ts.map +1 -0
- package/dist/cjs/lib/stream/transport/proto.js +118 -0
- package/dist/cjs/lib/stream/transport/proto.js.map +1 -0
- package/dist/cjs/lib/stream/transport/s2s/index.d.ts +3 -3
- package/dist/cjs/lib/stream/transport/s2s/index.d.ts.map +1 -1
- package/dist/cjs/lib/stream/transport/s2s/index.js +115 -82
- package/dist/cjs/lib/stream/transport/s2s/index.js.map +1 -1
- package/dist/cjs/lib/stream/types.d.ts +81 -36
- package/dist/cjs/lib/stream/types.d.ts.map +1 -1
- package/dist/cjs/lib/stream/types.js +18 -0
- package/dist/cjs/lib/stream/types.js.map +1 -1
- package/dist/cjs/metrics.d.ts +18 -17
- package/dist/cjs/metrics.d.ts.map +1 -1
- package/dist/cjs/metrics.js +67 -12
- package/dist/cjs/metrics.js.map +1 -1
- package/dist/cjs/producer.d.ts +82 -0
- package/dist/cjs/producer.d.ts.map +1 -0
- package/dist/cjs/producer.js +305 -0
- package/dist/cjs/producer.js.map +1 -0
- package/dist/cjs/s2.d.ts +1 -2
- package/dist/cjs/s2.d.ts.map +1 -1
- package/dist/cjs/s2.js +11 -15
- package/dist/cjs/s2.js.map +1 -1
- package/dist/cjs/stream.d.ts +26 -12
- package/dist/cjs/stream.d.ts.map +1 -1
- package/dist/cjs/stream.js +77 -13
- package/dist/cjs/stream.js.map +1 -1
- package/dist/cjs/streams.d.ts +29 -19
- package/dist/cjs/streams.d.ts.map +1 -1
- package/dist/cjs/streams.js +120 -9
- package/dist/cjs/streams.js.map +1 -1
- package/dist/cjs/types.d.ts +624 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +129 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils.d.ts +1 -22
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +0 -42
- package/dist/cjs/utils.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/accessTokens.d.ts +27 -14
- package/dist/esm/accessTokens.d.ts.map +1 -1
- package/dist/esm/accessTokens.js +73 -9
- package/dist/esm/accessTokens.js.map +1 -1
- package/dist/esm/basins.d.ts +29 -19
- package/dist/esm/basins.d.ts.map +1 -1
- package/dist/esm/basins.js +119 -9
- package/dist/esm/basins.js.map +1 -1
- package/dist/esm/batch-transform.d.ts +12 -16
- package/dist/esm/batch-transform.d.ts.map +1 -1
- package/dist/esm/batch-transform.js +18 -22
- package/dist/esm/batch-transform.js.map +1 -1
- package/dist/esm/common.d.ts +31 -24
- package/dist/esm/common.d.ts.map +1 -1
- package/dist/esm/common.js +20 -1
- package/dist/esm/common.js.map +1 -1
- package/dist/esm/endpoints.d.ts +63 -0
- package/dist/esm/endpoints.d.ts.map +1 -0
- package/dist/esm/endpoints.js +115 -0
- package/dist/esm/endpoints.js.map +1 -0
- package/dist/esm/error.d.ts.map +1 -1
- package/dist/esm/error.js +11 -0
- package/dist/esm/error.js.map +1 -1
- package/dist/esm/generated/types.gen.d.ts +11 -20
- package/dist/esm/generated/types.gen.d.ts.map +1 -1
- package/dist/esm/index.d.ts +30 -46
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +33 -19
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/case-transform.d.ts +59 -0
- package/dist/esm/internal/case-transform.d.ts.map +1 -0
- package/dist/esm/internal/case-transform.js +76 -0
- package/dist/esm/internal/case-transform.js.map +1 -0
- package/dist/esm/internal/mappers.d.ts +51 -0
- package/dist/esm/internal/mappers.d.ts.map +1 -0
- package/dist/esm/internal/mappers.js +218 -0
- package/dist/esm/internal/mappers.js.map +1 -0
- package/dist/esm/internal/sdk-types.d.ts +127 -0
- package/dist/esm/internal/sdk-types.d.ts.map +1 -0
- package/dist/esm/internal/sdk-types.js +8 -0
- package/dist/esm/internal/sdk-types.js.map +1 -0
- package/dist/esm/lib/base64.d.ts +8 -0
- package/dist/esm/lib/base64.d.ts.map +1 -1
- package/dist/esm/lib/base64.js +30 -11
- package/dist/esm/lib/base64.js.map +1 -1
- package/dist/esm/lib/event-stream.d.ts.map +1 -1
- package/dist/esm/lib/event-stream.js +2 -1
- package/dist/esm/lib/event-stream.js.map +1 -1
- package/dist/esm/lib/paginate.d.ts +57 -0
- package/dist/esm/lib/paginate.d.ts.map +1 -0
- package/dist/esm/lib/paginate.js +48 -0
- package/dist/esm/lib/paginate.js.map +1 -0
- package/dist/esm/lib/result.d.ts +1 -1
- package/dist/esm/lib/result.d.ts.map +1 -1
- package/dist/esm/lib/retry.d.ts +47 -31
- package/dist/esm/lib/retry.d.ts.map +1 -1
- package/dist/esm/lib/retry.js +303 -201
- package/dist/esm/lib/retry.js.map +1 -1
- package/dist/esm/lib/stream/runtime.d.ts +1 -1
- package/dist/esm/lib/stream/transport/fetch/index.d.ts +7 -9
- package/dist/esm/lib/stream/transport/fetch/index.d.ts.map +1 -1
- package/dist/esm/lib/stream/transport/fetch/index.js +40 -41
- package/dist/esm/lib/stream/transport/fetch/index.js.map +1 -1
- package/dist/esm/lib/stream/transport/fetch/shared.d.ts +7 -2
- package/dist/esm/lib/stream/transport/fetch/shared.d.ts.map +1 -1
- package/dist/esm/lib/stream/transport/fetch/shared.js +58 -112
- package/dist/esm/lib/stream/transport/fetch/shared.js.map +1 -1
- package/dist/esm/lib/stream/transport/proto.d.ts +9 -0
- package/dist/esm/lib/stream/transport/proto.d.ts.map +1 -0
- package/dist/esm/lib/stream/transport/proto.js +110 -0
- package/dist/esm/lib/stream/transport/proto.js.map +1 -0
- package/dist/esm/lib/stream/transport/s2s/index.d.ts +3 -3
- package/dist/esm/lib/stream/transport/s2s/index.d.ts.map +1 -1
- package/dist/esm/lib/stream/transport/s2s/index.js +116 -82
- package/dist/esm/lib/stream/transport/s2s/index.js.map +1 -1
- package/dist/esm/lib/stream/types.d.ts +81 -36
- package/dist/esm/lib/stream/types.d.ts.map +1 -1
- package/dist/esm/lib/stream/types.js +17 -1
- package/dist/esm/lib/stream/types.js.map +1 -1
- package/dist/esm/metrics.d.ts +18 -17
- package/dist/esm/metrics.d.ts.map +1 -1
- package/dist/esm/metrics.js +66 -12
- package/dist/esm/metrics.js.map +1 -1
- package/dist/esm/producer.d.ts +82 -0
- package/dist/esm/producer.d.ts.map +1 -0
- package/dist/esm/producer.js +300 -0
- package/dist/esm/producer.js.map +1 -0
- package/dist/esm/s2.d.ts +1 -2
- package/dist/esm/s2.d.ts.map +1 -1
- package/dist/esm/s2.js +12 -16
- package/dist/esm/s2.js.map +1 -1
- package/dist/esm/stream.d.ts +26 -12
- package/dist/esm/stream.d.ts.map +1 -1
- package/dist/esm/stream.js +79 -15
- package/dist/esm/stream.js.map +1 -1
- package/dist/esm/streams.d.ts +29 -19
- package/dist/esm/streams.d.ts.map +1 -1
- package/dist/esm/streams.js +120 -9
- package/dist/esm/streams.js.map +1 -1
- package/dist/esm/types.d.ts +624 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +126 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.d.ts +1 -22
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +0 -41
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +4 -3
package/dist/cjs/lib/retry.js
CHANGED
|
@@ -9,17 +9,63 @@ const debug_1 = require("debug");
|
|
|
9
9
|
const error_js_1 = require("../error.js");
|
|
10
10
|
const utils_js_1 = require("../utils.js");
|
|
11
11
|
const result_js_1 = require("./result.js");
|
|
12
|
+
const types_js_1 = require("./stream/types.js");
|
|
12
13
|
const debugWith = (0, debug_1.default)("s2:retry:with");
|
|
13
14
|
const debugRead = (0, debug_1.default)("s2:retry:read");
|
|
14
15
|
const debugSession = (0, debug_1.default)("s2:retry:session");
|
|
16
|
+
/** Type guard for errors with a code property (e.g., Node.js errors). */
|
|
17
|
+
function hasErrorCode(err, code) {
|
|
18
|
+
return (typeof err === "object" &&
|
|
19
|
+
err !== null &&
|
|
20
|
+
"code" in err &&
|
|
21
|
+
err.code === code);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Convert generated StreamPosition to SDK StreamPosition.
|
|
25
|
+
*/
|
|
26
|
+
function toSDKStreamPosition(pos) {
|
|
27
|
+
return {
|
|
28
|
+
seqNum: pos.seq_num,
|
|
29
|
+
timestamp: new Date(pos.timestamp),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert internal ReadRecord (with headers as object for strings) to SDK ReadRecord (with headers as array).
|
|
34
|
+
*/
|
|
35
|
+
function toSDKReadRecord(record) {
|
|
36
|
+
if (record.headers &&
|
|
37
|
+
typeof record.headers === "object" &&
|
|
38
|
+
!Array.isArray(record.headers)) {
|
|
39
|
+
// String format: headers is an object, convert to array of tuples
|
|
40
|
+
const result = {
|
|
41
|
+
seqNum: record.seq_num,
|
|
42
|
+
timestamp: new Date(record.timestamp),
|
|
43
|
+
body: record.body ?? "",
|
|
44
|
+
headers: Object.entries(record.headers),
|
|
45
|
+
};
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Bytes format: headers is already an array
|
|
50
|
+
const result = {
|
|
51
|
+
seqNum: record.seq_num,
|
|
52
|
+
timestamp: new Date(record.timestamp),
|
|
53
|
+
body: record.body ?? new Uint8Array(),
|
|
54
|
+
headers: record.headers ?? [],
|
|
55
|
+
};
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
15
59
|
/**
|
|
16
60
|
* Default retry configuration.
|
|
17
61
|
*/
|
|
18
62
|
exports.DEFAULT_RETRY_CONFIG = {
|
|
19
63
|
maxAttempts: 3,
|
|
20
|
-
|
|
64
|
+
minDelayMillis: 100,
|
|
65
|
+
maxDelayMillis: 1000,
|
|
21
66
|
appendRetryPolicy: "all",
|
|
22
67
|
requestTimeoutMillis: 5000, // 5 seconds
|
|
68
|
+
connectionTimeoutMillis: 5000, // 5 seconds
|
|
23
69
|
};
|
|
24
70
|
const RETRYABLE_STATUS_CODES = new Set([
|
|
25
71
|
408, // request_timeout
|
|
@@ -27,6 +73,7 @@ const RETRYABLE_STATUS_CODES = new Set([
|
|
|
27
73
|
500, // internal_server_error
|
|
28
74
|
502, // bad_gateway
|
|
29
75
|
503, // service_unavailable
|
|
76
|
+
504, // gateway_timeout
|
|
30
77
|
]);
|
|
31
78
|
/**
|
|
32
79
|
* Determines if an error should be retried based on its characteristics.
|
|
@@ -46,16 +93,25 @@ function isRetryable(error) {
|
|
|
46
93
|
return false;
|
|
47
94
|
}
|
|
48
95
|
/**
|
|
49
|
-
* Calculates the delay before the next retry attempt using
|
|
50
|
-
* with jitter.
|
|
51
|
-
*
|
|
96
|
+
* Calculates the delay before the next retry attempt using exponential backoff
|
|
97
|
+
* with additive jitter.
|
|
98
|
+
*
|
|
99
|
+
* Formula:
|
|
100
|
+
* baseDelay = min(minDelayMillis * 2^attempt, maxDelayMillis)
|
|
101
|
+
* jitter = random(0, baseDelay)
|
|
102
|
+
* delay = baseDelay + jitter
|
|
103
|
+
*
|
|
104
|
+
* @param attempt - Zero-based retry attempt number (0 = first retry)
|
|
105
|
+
* @param minDelayMillis - Minimum delay for exponential backoff
|
|
106
|
+
* @param maxDelayMillis - Maximum base delay (actual delay can be up to 2x with jitter)
|
|
52
107
|
*/
|
|
53
|
-
function calculateDelay(attempt,
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
108
|
+
function calculateDelay(attempt, minDelayMillis, maxDelayMillis) {
|
|
109
|
+
// Calculate exponential backoff: minDelay * 2^attempt, capped at maxDelay
|
|
110
|
+
const baseDelay = Math.min(minDelayMillis * Math.pow(2, attempt), maxDelayMillis);
|
|
111
|
+
// Add jitter: random value in [0, baseDelay)
|
|
112
|
+
const jitter = Math.random() * baseDelay;
|
|
113
|
+
// Total delay is base + jitter
|
|
114
|
+
return Math.floor(baseDelay + jitter);
|
|
59
115
|
}
|
|
60
116
|
/**
|
|
61
117
|
* Sleeps for the specified duration.
|
|
@@ -107,7 +163,7 @@ async function withRetries(retryConfig, fn, isPolicyCompliant = () => true) {
|
|
|
107
163
|
throw error;
|
|
108
164
|
}
|
|
109
165
|
// Calculate delay and wait before retrying
|
|
110
|
-
const delay = calculateDelay(attemptNo - 1, config.
|
|
166
|
+
const delay = calculateDelay(attemptNo - 1, config.minDelayMillis, config.maxDelayMillis);
|
|
111
167
|
debugWith("retryable error, backing off for %dms, status=%s", delay, error.status);
|
|
112
168
|
await sleep(delay);
|
|
113
169
|
}
|
|
@@ -136,16 +192,11 @@ class RetryReadSession extends ReadableStream {
|
|
|
136
192
|
return new RetryReadSession(args, generator, config, session);
|
|
137
193
|
}
|
|
138
194
|
catch (err) {
|
|
139
|
-
const error =
|
|
140
|
-
? err
|
|
141
|
-
: new error_js_1.S2Error({
|
|
142
|
-
message: String(err),
|
|
143
|
-
status: 502,
|
|
144
|
-
});
|
|
195
|
+
const error = (0, error_js_1.s2Error)(err);
|
|
145
196
|
lastError = error;
|
|
146
197
|
const effectiveMax = Math.max(1, retryConfig.maxAttempts);
|
|
147
198
|
if (isRetryable(error) && attempt < effectiveMax - 1) {
|
|
148
|
-
const delay = calculateDelay(attempt, retryConfig.
|
|
199
|
+
const delay = calculateDelay(attempt, retryConfig.minDelayMillis, retryConfig.maxDelayMillis);
|
|
149
200
|
debugRead("connection error in create, will retry after %dms, status=%s", delay, error.status);
|
|
150
201
|
await sleep(delay);
|
|
151
202
|
attempt++;
|
|
@@ -181,16 +232,11 @@ class RetryReadSession extends ReadableStream {
|
|
|
181
232
|
}
|
|
182
233
|
catch (err) {
|
|
183
234
|
// Convert to S2Error if needed
|
|
184
|
-
const error =
|
|
185
|
-
? err
|
|
186
|
-
: new error_js_1.S2Error({
|
|
187
|
-
message: String(err),
|
|
188
|
-
status: 502, // Bad Gateway - connection failure
|
|
189
|
-
});
|
|
235
|
+
const error = (0, error_js_1.s2Error)(err);
|
|
190
236
|
// Check if we can retry connection errors
|
|
191
237
|
const effectiveMax = Math.max(1, retryConfig.maxAttempts);
|
|
192
238
|
if (isRetryable(error) && attempt < effectiveMax - 1) {
|
|
193
|
-
const delay = calculateDelay(attempt, retryConfig.
|
|
239
|
+
const delay = calculateDelay(attempt, retryConfig.minDelayMillis, retryConfig.maxDelayMillis);
|
|
194
240
|
debugRead("connection error, will retry after %dms, status=%s", delay, error.status);
|
|
195
241
|
await sleep(delay);
|
|
196
242
|
attempt++;
|
|
@@ -231,7 +277,7 @@ class RetryReadSession extends ReadableStream {
|
|
|
231
277
|
delete nextArgs.tail_offset;
|
|
232
278
|
}
|
|
233
279
|
// Compute planned backoff delay now so we can subtract it from wait budget
|
|
234
|
-
const delay = calculateDelay(attempt, retryConfig.
|
|
280
|
+
const delay = calculateDelay(attempt, retryConfig.minDelayMillis, retryConfig.maxDelayMillis);
|
|
235
281
|
// Recompute remaining budget from original request each time to avoid double-subtraction
|
|
236
282
|
if (baselineCount !== undefined) {
|
|
237
283
|
nextArgs.count = Math.max(0, baselineCount - this._recordsRead);
|
|
@@ -242,7 +288,7 @@ class RetryReadSession extends ReadableStream {
|
|
|
242
288
|
// Adjust wait from original budget based on total elapsed time since start
|
|
243
289
|
if (baselineWait !== undefined) {
|
|
244
290
|
const elapsedSeconds = (performance.now() - startTimeMs) / 1000;
|
|
245
|
-
nextArgs.wait = Math.max(0, baselineWait - (elapsedSeconds + delay / 1000));
|
|
291
|
+
nextArgs.wait = Math.max(0, Math.floor(baselineWait - (elapsedSeconds + delay / 1000)));
|
|
246
292
|
}
|
|
247
293
|
// Proactively cancel the current transport session before retrying
|
|
248
294
|
try {
|
|
@@ -270,7 +316,7 @@ class RetryReadSession extends ReadableStream {
|
|
|
270
316
|
this._recordsRead++;
|
|
271
317
|
this._bytesRead += (0, utils_js_1.meteredBytes)(record);
|
|
272
318
|
attempt = 0;
|
|
273
|
-
controller.enqueue(record);
|
|
319
|
+
controller.enqueue(toSDKReadRecord(record));
|
|
274
320
|
}
|
|
275
321
|
}
|
|
276
322
|
},
|
|
@@ -280,7 +326,7 @@ class RetryReadSession extends ReadableStream {
|
|
|
280
326
|
}
|
|
281
327
|
catch (err) {
|
|
282
328
|
// Ignore ERR_INVALID_STATE - stream may already be closed/cancelled
|
|
283
|
-
if (err
|
|
329
|
+
if (!hasErrorCode(err, "ERR_INVALID_STATE")) {
|
|
284
330
|
throw err;
|
|
285
331
|
}
|
|
286
332
|
}
|
|
@@ -292,7 +338,8 @@ class RetryReadSession extends ReadableStream {
|
|
|
292
338
|
}
|
|
293
339
|
// Polyfill for older browsers / Node.js environments
|
|
294
340
|
[Symbol.asyncIterator]() {
|
|
295
|
-
const
|
|
341
|
+
const proto = ReadableStream.prototype;
|
|
342
|
+
const fn = proto[Symbol.asyncIterator];
|
|
296
343
|
if (typeof fn === "function") {
|
|
297
344
|
try {
|
|
298
345
|
return fn.call(this);
|
|
@@ -317,7 +364,7 @@ class RetryReadSession extends ReadableStream {
|
|
|
317
364
|
await reader.cancel(e);
|
|
318
365
|
}
|
|
319
366
|
catch (err) {
|
|
320
|
-
if (err
|
|
367
|
+
if (!hasErrorCode(err, "ERR_INVALID_STATE"))
|
|
321
368
|
throw err;
|
|
322
369
|
}
|
|
323
370
|
reader.releaseLock();
|
|
@@ -328,7 +375,7 @@ class RetryReadSession extends ReadableStream {
|
|
|
328
375
|
await reader.cancel("done");
|
|
329
376
|
}
|
|
330
377
|
catch (err) {
|
|
331
|
-
if (err
|
|
378
|
+
if (!hasErrorCode(err, "ERR_INVALID_STATE"))
|
|
332
379
|
throw err;
|
|
333
380
|
}
|
|
334
381
|
reader.releaseLock();
|
|
@@ -340,14 +387,19 @@ class RetryReadSession extends ReadableStream {
|
|
|
340
387
|
};
|
|
341
388
|
}
|
|
342
389
|
lastObservedTail() {
|
|
343
|
-
return this._lastObservedTail
|
|
390
|
+
return this._lastObservedTail
|
|
391
|
+
? toSDKStreamPosition(this._lastObservedTail)
|
|
392
|
+
: undefined;
|
|
344
393
|
}
|
|
345
394
|
nextReadPosition() {
|
|
346
|
-
return this._nextReadPosition
|
|
395
|
+
return this._nextReadPosition
|
|
396
|
+
? toSDKStreamPosition(this._nextReadPosition)
|
|
397
|
+
: undefined;
|
|
347
398
|
}
|
|
348
399
|
}
|
|
349
400
|
exports.RetryReadSession = RetryReadSession;
|
|
350
|
-
const
|
|
401
|
+
const MIN_MAX_INFLIGHT_BYTES = 1 * 1024 * 1024; // 1 MiB minimum
|
|
402
|
+
const DEFAULT_MAX_INFLIGHT_BYTES = 3 * 1024 * 1024; // 3 MiB default
|
|
351
403
|
class RetryAppendSession {
|
|
352
404
|
generator;
|
|
353
405
|
sessionOptions;
|
|
@@ -356,10 +408,11 @@ class RetryAppendSession {
|
|
|
356
408
|
maxInflightBatches;
|
|
357
409
|
retryConfig;
|
|
358
410
|
inflight = [];
|
|
359
|
-
|
|
411
|
+
capacityWaiters = []; // Queue of waiters for capacity
|
|
360
412
|
session;
|
|
361
413
|
queuedBytes = 0;
|
|
362
414
|
pendingBytes = 0;
|
|
415
|
+
pendingBatches = 0;
|
|
363
416
|
consecutiveFailures = 0;
|
|
364
417
|
currentAttempt = 0;
|
|
365
418
|
pumpPromise;
|
|
@@ -372,6 +425,7 @@ class RetryAppendSession {
|
|
|
372
425
|
acksController;
|
|
373
426
|
readable;
|
|
374
427
|
writable;
|
|
428
|
+
streamName;
|
|
375
429
|
/**
|
|
376
430
|
* If the session has failed, returns the original fatal error that caused
|
|
377
431
|
* the pump to stop. Returns undefined when the session has not failed.
|
|
@@ -379,17 +433,22 @@ class RetryAppendSession {
|
|
|
379
433
|
failureCause() {
|
|
380
434
|
return this.fatalError;
|
|
381
435
|
}
|
|
382
|
-
constructor(generator, sessionOptions, config) {
|
|
436
|
+
constructor(generator, sessionOptions, config, streamName) {
|
|
383
437
|
this.generator = generator;
|
|
384
438
|
this.sessionOptions = sessionOptions;
|
|
439
|
+
this.streamName = streamName ?? "unknown";
|
|
385
440
|
this.retryConfig = {
|
|
386
441
|
...exports.DEFAULT_RETRY_CONFIG,
|
|
387
442
|
...config,
|
|
388
443
|
};
|
|
389
444
|
this.requestTimeoutMillis = this.retryConfig.requestTimeoutMillis;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
445
|
+
// Clamp maxInflightBytes to at least 1 MiB
|
|
446
|
+
this.maxQueuedBytes = Math.max(MIN_MAX_INFLIGHT_BYTES, this.sessionOptions?.maxInflightBytes ?? DEFAULT_MAX_INFLIGHT_BYTES);
|
|
447
|
+
// Clamp maxInflightBatches to at least 1 if set
|
|
448
|
+
this.maxInflightBatches =
|
|
449
|
+
this.sessionOptions?.maxInflightBatches !== undefined
|
|
450
|
+
? Math.max(1, this.sessionOptions.maxInflightBatches)
|
|
451
|
+
: undefined;
|
|
393
452
|
this.readable = new ReadableStream({
|
|
394
453
|
start: (controller) => {
|
|
395
454
|
this.acksController = controller;
|
|
@@ -397,25 +456,16 @@ class RetryAppendSession {
|
|
|
397
456
|
});
|
|
398
457
|
this.writable = new WritableStream({
|
|
399
458
|
write: async (chunk) => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
: [chunk.records];
|
|
403
|
-
// Calculate metered size
|
|
404
|
-
let batchMeteredSize = 0;
|
|
405
|
-
for (const record of recordsArray) {
|
|
406
|
-
batchMeteredSize += (0, utils_js_1.meteredBytes)(record);
|
|
459
|
+
if (this.closed || this.closing) {
|
|
460
|
+
throw new error_js_1.S2Error({ message: "AppendSession is closed" });
|
|
407
461
|
}
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Submit without waiting for ack (writable doesn't need per-batch resolution)
|
|
416
|
-
const promise = this.submitInternal(recordsArray, args, batchMeteredSize);
|
|
417
|
-
promise.catch(() => {
|
|
418
|
-
// Swallow to avoid unhandled rejection; pump surfaces errors via readable stream
|
|
462
|
+
// chunk is already AppendInput with meteredBytes computed
|
|
463
|
+
// Reuse submit() to leverage shared backpressure/pump logic.
|
|
464
|
+
const ticket = await this.submit(chunk);
|
|
465
|
+
// Writable stream API only needs enqueue semantics, so drop ack but
|
|
466
|
+
// suppress rejection noise (pump surfaces fatal errors elsewhere).
|
|
467
|
+
ticket.ack().catch(() => {
|
|
468
|
+
// Intentionally ignored.
|
|
419
469
|
});
|
|
420
470
|
},
|
|
421
471
|
close: async () => {
|
|
@@ -427,57 +477,104 @@ class RetryAppendSession {
|
|
|
427
477
|
},
|
|
428
478
|
});
|
|
429
479
|
}
|
|
430
|
-
static async create(generator, sessionOptions, config) {
|
|
431
|
-
return new RetryAppendSession(generator, sessionOptions, config);
|
|
480
|
+
static async create(generator, sessionOptions, config, streamName) {
|
|
481
|
+
return new RetryAppendSession(generator, sessionOptions, config, streamName);
|
|
432
482
|
}
|
|
433
483
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
484
|
+
* Wait for capacity to be available for the given batch size.
|
|
485
|
+
* Call this before submit() to apply backpressure based on maxInflightBatches/maxInflightBytes.
|
|
486
|
+
*
|
|
487
|
+
* @param bytes - Size in bytes (use meteredBytes() to calculate)
|
|
488
|
+
* @param numBatches - Number of batches (default: 1)
|
|
489
|
+
* @returns Promise that resolves when capacity is available
|
|
436
490
|
*/
|
|
437
|
-
async
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
491
|
+
async waitForCapacity(bytes, numBatches = 1) {
|
|
492
|
+
debugSession("[%s] [CAPACITY] checking for %d bytes, %d batches: queuedBytes=%d, pendingBytes=%d, maxQueuedBytes=%d, inflight=%d, pendingBatches=%d, maxInflightBatches=%s", this.streamName, bytes, numBatches, this.queuedBytes, this.pendingBytes, this.maxQueuedBytes, this.inflight.length, this.pendingBatches, this.maxInflightBatches ?? "unlimited");
|
|
493
|
+
// Check if we have capacity
|
|
494
|
+
while (true) {
|
|
495
|
+
// Check for fatal error before adding to pendingBytes
|
|
496
|
+
if (this.fatalError) {
|
|
497
|
+
debugSession("[%s] [CAPACITY] fatal error detected, rejecting: %s", this.streamName, this.fatalError.message);
|
|
498
|
+
throw this.fatalError;
|
|
444
499
|
}
|
|
500
|
+
// Byte-based gating
|
|
501
|
+
if (this.queuedBytes + this.pendingBytes + bytes <= this.maxQueuedBytes) {
|
|
502
|
+
// Batch-based gating (if configured)
|
|
503
|
+
if (this.maxInflightBatches === undefined ||
|
|
504
|
+
this.inflight.length + this.pendingBatches + numBatches <=
|
|
505
|
+
this.maxInflightBatches) {
|
|
506
|
+
debugSession("[%s] [CAPACITY] capacity available, adding %d to pendingBytes and %d to pendingBatches", this.streamName, bytes, numBatches);
|
|
507
|
+
this.pendingBytes += bytes;
|
|
508
|
+
this.pendingBatches += numBatches;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// No capacity - wait in queue
|
|
513
|
+
debugSession("[%s] [CAPACITY] no capacity, waiting for release", this.streamName);
|
|
514
|
+
await new Promise((resolve) => {
|
|
515
|
+
this.capacityWaiters.push({
|
|
516
|
+
resolve,
|
|
517
|
+
bytes,
|
|
518
|
+
batches: numBatches,
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
debugSession("[%s] [CAPACITY] woke up, rechecking", this.streamName);
|
|
445
522
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Submit an append request.
|
|
526
|
+
* Returns a promise that resolves to a submit ticket once the batch is enqueued (has capacity).
|
|
527
|
+
* The ticket's ack() can be awaited to get the AppendAck once the batch is durable.
|
|
528
|
+
* This method applies backpressure and will block if capacity limits are reached.
|
|
529
|
+
*/
|
|
530
|
+
async submit(input) {
|
|
531
|
+
if (this.closed || this.closing) {
|
|
532
|
+
return Promise.reject(new error_js_1.S2Error({ message: "AppendSession is closed" }));
|
|
533
|
+
}
|
|
534
|
+
// Use cached metered size from AppendInput
|
|
535
|
+
const batchMeteredSize = input.meteredBytes;
|
|
536
|
+
// This needs to happen in the sync path.
|
|
537
|
+
this.ensurePump();
|
|
538
|
+
// Wait for capacity (this is where backpressure is applied - outer promise resolves when enqueued)
|
|
539
|
+
await this.waitForCapacity(batchMeteredSize, 1);
|
|
540
|
+
// Move reserved bytes and batches to queued accounting before submission
|
|
541
|
+
this.pendingBytes = Math.max(0, this.pendingBytes - batchMeteredSize);
|
|
542
|
+
this.pendingBatches = Math.max(0, this.pendingBatches - 1);
|
|
543
|
+
// Create the inner promise that resolves when durable
|
|
544
|
+
const innerPromise = this.submitInternal(input, batchMeteredSize).then((result) => {
|
|
545
|
+
if (result.ok) {
|
|
546
|
+
return result.value;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
throw result.error;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
// Prevent early rejections from surfacing as unhandled when callers delay ack()
|
|
553
|
+
innerPromise.catch(() => { });
|
|
554
|
+
// Return ticket immediately (outer promise has resolved via waitForCapacity)
|
|
555
|
+
return new types_js_1.BatchSubmitTicket(innerPromise, batchMeteredSize, input.records.length);
|
|
454
556
|
}
|
|
455
557
|
/**
|
|
456
558
|
* Internal submit that returns discriminated union.
|
|
457
559
|
* Creates inflight entry and starts pump if needed.
|
|
458
560
|
*/
|
|
459
|
-
submitInternal(
|
|
460
|
-
if (this.closed || this.closing) {
|
|
461
|
-
return Promise.resolve((0, result_js_1.err)(new error_js_1.S2Error({ message: "AppendSession is closed", status: 400 })));
|
|
462
|
-
}
|
|
561
|
+
submitInternal(input, batchMeteredSize) {
|
|
463
562
|
// Check for fatal error (e.g., from abort())
|
|
464
563
|
if (this.fatalError) {
|
|
465
|
-
debugSession("[SUBMIT] rejecting due to fatal error: %s", this.fatalError.message);
|
|
564
|
+
debugSession("[%s] [SUBMIT] rejecting due to fatal error: %s", this.streamName, this.fatalError.message);
|
|
466
565
|
return Promise.resolve((0, result_js_1.err)(this.fatalError));
|
|
467
566
|
}
|
|
468
567
|
// Create promise for submit() callers
|
|
469
568
|
return new Promise((resolve) => {
|
|
470
569
|
// Create inflight entry (innerPromise will be set when pump processes it)
|
|
471
570
|
const entry = {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
expectedCount: records.length,
|
|
475
|
-
meteredBytes: batchMeteredSize,
|
|
571
|
+
input,
|
|
572
|
+
expectedCount: input.records.length,
|
|
476
573
|
innerPromise: new Promise(() => { }), // Never-resolving placeholder
|
|
477
574
|
maybeResolve: resolve,
|
|
478
575
|
needsSubmit: true, // Mark for pump to submit
|
|
479
576
|
};
|
|
480
|
-
debugSession("[SUBMIT] enqueueing %d records (%d bytes): inflight=%d->%d, queuedBytes=%d->%d", records.length, batchMeteredSize, this.inflight.length, this.inflight.length + 1, this.queuedBytes, this.queuedBytes + batchMeteredSize);
|
|
577
|
+
debugSession("[%s] [SUBMIT] enqueueing %d records (%d bytes), match_seq_num=%s: inflight=%d->%d, queuedBytes=%d->%d", this.streamName, input.records.length, batchMeteredSize, input.matchSeqNum ?? "none", this.inflight.length, this.inflight.length + 1, this.queuedBytes, this.queuedBytes + batchMeteredSize);
|
|
481
578
|
this.inflight.push(entry);
|
|
482
579
|
this.queuedBytes += batchMeteredSize;
|
|
483
580
|
// Wake pump if it's sleeping
|
|
@@ -488,50 +585,42 @@ class RetryAppendSession {
|
|
|
488
585
|
this.ensurePump();
|
|
489
586
|
});
|
|
490
587
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Wait for capacity before allowing write to proceed (writable only).
|
|
493
|
-
*/
|
|
494
|
-
async waitForCapacity(bytes) {
|
|
495
|
-
debugSession("[CAPACITY] checking for %d bytes: queuedBytes=%d, pendingBytes=%d, maxQueuedBytes=%d, inflight=%d", bytes, this.queuedBytes, this.pendingBytes, this.maxQueuedBytes, this.inflight.length);
|
|
496
|
-
// Check if we have capacity
|
|
497
|
-
while (true) {
|
|
498
|
-
// Check for fatal error before adding to pendingBytes
|
|
499
|
-
if (this.fatalError) {
|
|
500
|
-
debugSession("[CAPACITY] fatal error detected, rejecting: %s", this.fatalError.message);
|
|
501
|
-
throw this.fatalError;
|
|
502
|
-
}
|
|
503
|
-
// Byte-based gating
|
|
504
|
-
if (this.queuedBytes + this.pendingBytes + bytes <= this.maxQueuedBytes) {
|
|
505
|
-
// Batch-based gating (if configured)
|
|
506
|
-
if (this.maxInflightBatches === undefined ||
|
|
507
|
-
this.inflight.length < this.maxInflightBatches) {
|
|
508
|
-
debugSession("[CAPACITY] capacity available, adding %d to pendingBytes", bytes);
|
|
509
|
-
this.pendingBytes += bytes;
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
// No capacity - wait
|
|
514
|
-
// WritableStream enforces writer lock, so only one write can be blocked at a time
|
|
515
|
-
debugSession("[CAPACITY] no capacity, waiting for release");
|
|
516
|
-
await new Promise((resolve) => {
|
|
517
|
-
this.capacityWaiter = resolve;
|
|
518
|
-
});
|
|
519
|
-
debugSession("[CAPACITY] woke up, rechecking");
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
588
|
/**
|
|
523
589
|
* Release capacity and wake waiter if present.
|
|
524
590
|
*/
|
|
525
591
|
releaseCapacity(bytes) {
|
|
526
|
-
debugSession("[CAPACITY] releasing %d bytes: queuedBytes=%d->%d, pendingBytes=%d->%d,
|
|
592
|
+
debugSession("[%s] [CAPACITY] releasing %d bytes: queuedBytes=%d->%d, pendingBytes=%d->%d, pendingBatches=%d, numWaiters=%d", this.streamName, bytes, this.queuedBytes, this.queuedBytes - bytes, this.pendingBytes, Math.max(0, this.pendingBytes - bytes), this.pendingBatches, this.capacityWaiters.length);
|
|
527
593
|
this.queuedBytes -= bytes;
|
|
528
594
|
this.pendingBytes = Math.max(0, this.pendingBytes - bytes);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
595
|
+
this.wakeCapacityWaiters();
|
|
596
|
+
}
|
|
597
|
+
wakeCapacityWaiters() {
|
|
598
|
+
if (this.capacityWaiters.length === 0) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
let availableBytes = Math.max(0, this.maxQueuedBytes - (this.queuedBytes + this.pendingBytes));
|
|
602
|
+
let availableBatches = this.maxInflightBatches === undefined
|
|
603
|
+
? Number.POSITIVE_INFINITY
|
|
604
|
+
: Math.max(0, this.maxInflightBatches -
|
|
605
|
+
(this.inflight.length + this.pendingBatches));
|
|
606
|
+
while (this.capacityWaiters.length > 0) {
|
|
607
|
+
const next = this.capacityWaiters[0];
|
|
608
|
+
const needsBytes = next.bytes;
|
|
609
|
+
const needsBatches = next.batches;
|
|
610
|
+
const hasBatchCapacity = this.maxInflightBatches === undefined ||
|
|
611
|
+
needsBatches <= availableBatches;
|
|
612
|
+
if (needsBytes <= availableBytes && hasBatchCapacity) {
|
|
613
|
+
this.capacityWaiters.shift();
|
|
614
|
+
availableBytes -= needsBytes;
|
|
615
|
+
if (this.maxInflightBatches !== undefined) {
|
|
616
|
+
availableBatches -= needsBatches;
|
|
617
|
+
}
|
|
618
|
+
debugSession("[%s] [CAPACITY] waking waiter (bytes=%d, batches=%d)", this.streamName, needsBytes, needsBatches);
|
|
619
|
+
next.resolve();
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
// Not enough capacity for the next waiter yet - keep them queued.
|
|
623
|
+
break;
|
|
535
624
|
}
|
|
536
625
|
}
|
|
537
626
|
/**
|
|
@@ -542,7 +631,7 @@ class RetryAppendSession {
|
|
|
542
631
|
return;
|
|
543
632
|
}
|
|
544
633
|
this.pumpPromise = this.runPump().catch((e) => {
|
|
545
|
-
debugSession("pump crashed unexpectedly: %s", e);
|
|
634
|
+
debugSession("[%s] pump crashed unexpectedly: %s", this.streamName, e);
|
|
546
635
|
// This should never happen - pump handles all errors internally
|
|
547
636
|
});
|
|
548
637
|
}
|
|
@@ -550,93 +639,99 @@ class RetryAppendSession {
|
|
|
550
639
|
* Main pump loop: processes inflight queue, handles acks, retries, and recovery.
|
|
551
640
|
*/
|
|
552
641
|
async runPump() {
|
|
553
|
-
debugSession("pump started");
|
|
642
|
+
debugSession("[%s] pump started", this.streamName);
|
|
554
643
|
while (true) {
|
|
555
|
-
debugSession("[PUMP] loop: inflight=%d, queuedBytes=%d, pendingBytes=%d, closing=%s, pumpStopped=%s", this.inflight.length, this.queuedBytes, this.pendingBytes, this.closing, this.pumpStopped);
|
|
644
|
+
debugSession("[%s] [PUMP] loop: inflight=%d, queuedBytes=%d, pendingBytes=%d, pendingBatches=%d, closing=%s, pumpStopped=%s", this.streamName, this.inflight.length, this.queuedBytes, this.pendingBytes, this.pendingBatches, this.closing, this.pumpStopped);
|
|
556
645
|
// Check if we should stop
|
|
557
646
|
if (this.pumpStopped) {
|
|
558
|
-
debugSession("[PUMP] stopped by flag");
|
|
647
|
+
debugSession("[%s] [PUMP] stopped by flag", this.streamName);
|
|
559
648
|
return;
|
|
560
649
|
}
|
|
561
650
|
// If closing and queue is empty, stop
|
|
562
|
-
if
|
|
563
|
-
|
|
651
|
+
// BUT: if there are capacity waiters, they might add to inflight, so keep running
|
|
652
|
+
if (this.closing &&
|
|
653
|
+
this.inflight.length === 0 &&
|
|
654
|
+
this.capacityWaiters.length === 0) {
|
|
655
|
+
debugSession("[%s] [PUMP] closing and queue empty, stopping", this.streamName);
|
|
564
656
|
this.pumpStopped = true;
|
|
565
657
|
return;
|
|
566
658
|
}
|
|
567
659
|
// If no entries, sleep and continue
|
|
568
660
|
if (this.inflight.length === 0) {
|
|
569
|
-
debugSession("[PUMP] no entries,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
new Promise((resolve) => {
|
|
574
|
-
this.pumpWakeup = resolve;
|
|
575
|
-
}),
|
|
576
|
-
]);
|
|
661
|
+
debugSession("[%s] [PUMP] no entries, parking until wakeup", this.streamName);
|
|
662
|
+
await new Promise((resolve) => {
|
|
663
|
+
this.pumpWakeup = resolve;
|
|
664
|
+
});
|
|
577
665
|
this.pumpWakeup = undefined;
|
|
578
666
|
continue;
|
|
579
667
|
}
|
|
580
668
|
// Get head entry (we know it exists because we checked length above)
|
|
581
669
|
const head = this.inflight[0];
|
|
582
|
-
debugSession("[PUMP] processing head: expectedCount=%d, meteredBytes=%d", head.expectedCount, head.meteredBytes);
|
|
670
|
+
debugSession("[%s] [PUMP] processing head: expectedCount=%d, meteredBytes=%d, match_seq_num=%s", this.streamName, head.expectedCount, head.input.meteredBytes, head.input.matchSeqNum ?? "none");
|
|
583
671
|
// Ensure session exists
|
|
584
|
-
debugSession("[PUMP] ensuring session exists");
|
|
672
|
+
debugSession("[%s] [PUMP] ensuring session exists", this.streamName);
|
|
585
673
|
await this.ensureSession();
|
|
586
674
|
if (!this.session) {
|
|
587
675
|
// Session creation failed - will retry
|
|
588
|
-
|
|
589
|
-
|
|
676
|
+
this.consecutiveFailures++;
|
|
677
|
+
const delay = calculateDelay(this.consecutiveFailures - 1, this.retryConfig.minDelayMillis, this.retryConfig.maxDelayMillis);
|
|
678
|
+
debugSession("[%s] [PUMP] session creation failed, backing off for %dms", this.streamName, delay);
|
|
679
|
+
await sleep(delay);
|
|
590
680
|
continue;
|
|
591
681
|
}
|
|
592
682
|
// Submit ALL entries that need submitting (enables HTTP/2 pipelining for S2S)
|
|
593
683
|
for (const entry of this.inflight) {
|
|
594
684
|
if (!entry.innerPromise || entry.needsSubmit) {
|
|
595
|
-
debugSession("[PUMP] submitting entry to inner session (%d records, %d bytes)", entry.expectedCount, entry.meteredBytes);
|
|
596
|
-
|
|
597
|
-
entry.
|
|
685
|
+
debugSession("[%s] [PUMP] submitting entry to inner session (%d records, %d bytes, match_seq_num=%s)", this.streamName, entry.expectedCount, entry.input.meteredBytes, entry.input.matchSeqNum ?? "none");
|
|
686
|
+
const attemptStarted = performance.now();
|
|
687
|
+
entry.attemptStartedMonotonicMs = attemptStarted;
|
|
688
|
+
entry.innerPromise = this.session.submit(entry.input);
|
|
598
689
|
delete entry.needsSubmit;
|
|
599
690
|
}
|
|
600
691
|
}
|
|
601
692
|
// Wait for head with timeout
|
|
602
|
-
debugSession("[PUMP] waiting for head result");
|
|
693
|
+
debugSession("[%s] [PUMP] waiting for head result", this.streamName);
|
|
603
694
|
const result = await this.waitForHead(head);
|
|
604
|
-
debugSession("[PUMP] got result: kind=%s", result.kind);
|
|
695
|
+
debugSession("[%s] [PUMP] got result: kind=%s", this.streamName, result.kind);
|
|
696
|
+
// Convert result to AppendResult (timeout becomes retryable error)
|
|
697
|
+
let appendResult;
|
|
605
698
|
if (result.kind === "timeout") {
|
|
606
|
-
// Ack timeout -
|
|
699
|
+
// Ack timeout - convert to retryable error that flows through retry logic
|
|
607
700
|
const attemptElapsed = head.attemptStartedMonotonicMs != null
|
|
608
701
|
? Math.round(performance.now() - head.attemptStartedMonotonicMs)
|
|
609
702
|
: undefined;
|
|
610
703
|
const error = new error_js_1.S2Error({
|
|
611
|
-
message: `Request timeout after ${attemptElapsed ?? "unknown"}ms (${head.expectedCount} records, ${head.meteredBytes} bytes)`,
|
|
704
|
+
message: `Request timeout after ${attemptElapsed ?? "unknown"}ms (${head.expectedCount} records, ${head.input.meteredBytes} bytes)`,
|
|
612
705
|
status: 408,
|
|
613
706
|
code: "REQUEST_TIMEOUT",
|
|
707
|
+
origin: "sdk",
|
|
614
708
|
});
|
|
615
|
-
debugSession("ack timeout for head entry: %s", error.message);
|
|
616
|
-
|
|
617
|
-
|
|
709
|
+
debugSession("[%s] ack timeout for head entry: %s", this.streamName, error.message);
|
|
710
|
+
appendResult = (0, result_js_1.err)(error);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
// Promise settled
|
|
714
|
+
appendResult = result.value;
|
|
618
715
|
}
|
|
619
|
-
// Promise settled
|
|
620
|
-
const appendResult = result.value;
|
|
621
716
|
if (appendResult.ok) {
|
|
622
717
|
// Success!
|
|
623
718
|
const ack = appendResult.value;
|
|
624
|
-
debugSession("[PUMP] success, got ack",
|
|
719
|
+
debugSession("[%s] [PUMP] success, got ack: seq_num=%d-%d", this.streamName, ack.start.seqNum, ack.end.seqNum);
|
|
625
720
|
// Invariant check: ack count matches batch count
|
|
626
|
-
const ackCount =
|
|
721
|
+
const ackCount = ack.end.seqNum - ack.start.seqNum;
|
|
627
722
|
if (ackCount !== head.expectedCount) {
|
|
628
723
|
const error = (0, error_js_1.invariantViolation)(`Ack count mismatch: expected ${head.expectedCount}, got ${ackCount}`);
|
|
629
|
-
debugSession("invariant violation: %s", error.message);
|
|
724
|
+
debugSession("[%s] invariant violation: %s", this.streamName, error.message);
|
|
630
725
|
await this.abort(error);
|
|
631
726
|
return;
|
|
632
727
|
}
|
|
633
728
|
// Invariant check: sequence numbers must be strictly increasing
|
|
634
729
|
if (this._lastAckedPosition) {
|
|
635
|
-
const prevEnd =
|
|
636
|
-
const currentEnd =
|
|
730
|
+
const prevEnd = this._lastAckedPosition.end.seqNum;
|
|
731
|
+
const currentEnd = ack.end.seqNum;
|
|
637
732
|
if (currentEnd <= prevEnd) {
|
|
638
733
|
const error = (0, error_js_1.invariantViolation)(`Sequence number not strictly increasing: previous=${prevEnd}, current=${currentEnd}`);
|
|
639
|
-
debugSession("invariant violation: %s", error.message);
|
|
734
|
+
debugSession("[%s] invariant violation: %s", this.streamName, error.message);
|
|
640
735
|
await this.abort(error);
|
|
641
736
|
return;
|
|
642
737
|
}
|
|
@@ -652,12 +747,12 @@ class RetryAppendSession {
|
|
|
652
747
|
this.acksController?.enqueue(ack);
|
|
653
748
|
}
|
|
654
749
|
catch (e) {
|
|
655
|
-
debugSession("failed to enqueue ack: %s", e);
|
|
750
|
+
debugSession("[%s] failed to enqueue ack: %s", this.streamName, e);
|
|
656
751
|
}
|
|
657
752
|
// Remove from inflight and release capacity
|
|
658
|
-
debugSession("[PUMP] removing head from inflight, releasing %d bytes", head.meteredBytes);
|
|
753
|
+
debugSession("[%s] [PUMP] removing head from inflight, releasing %d bytes", this.streamName, head.input.meteredBytes);
|
|
659
754
|
this.inflight.shift();
|
|
660
|
-
this.releaseCapacity(head.meteredBytes);
|
|
755
|
+
this.releaseCapacity(head.input.meteredBytes);
|
|
661
756
|
// Reset consecutive failures on success
|
|
662
757
|
this.consecutiveFailures = 0;
|
|
663
758
|
this.currentAttempt = 0;
|
|
@@ -665,17 +760,17 @@ class RetryAppendSession {
|
|
|
665
760
|
else {
|
|
666
761
|
// Error result
|
|
667
762
|
const error = appendResult.error;
|
|
668
|
-
debugSession("[PUMP] error: status=%s, message=%s", error.status, error.message);
|
|
763
|
+
debugSession("[%s] [PUMP] error: status=%s, message=%s", this.streamName, error.status, error.message);
|
|
669
764
|
// Check if retryable
|
|
670
765
|
if (!isRetryable(error)) {
|
|
671
|
-
debugSession("error not retryable, aborting");
|
|
766
|
+
debugSession("[%s] error not retryable, aborting", this.streamName);
|
|
672
767
|
await this.abort(error);
|
|
673
768
|
return;
|
|
674
769
|
}
|
|
675
770
|
// Check policy compliance
|
|
676
771
|
if (this.retryConfig.appendRetryPolicy === "noSideEffects" &&
|
|
677
772
|
!this.isIdempotent(head)) {
|
|
678
|
-
debugSession("error not policy-compliant (noSideEffects), aborting");
|
|
773
|
+
debugSession("[%s] error not policy-compliant (noSideEffects), aborting", this.streamName);
|
|
679
774
|
await this.abort(error);
|
|
680
775
|
return;
|
|
681
776
|
}
|
|
@@ -683,7 +778,7 @@ class RetryAppendSession {
|
|
|
683
778
|
const effectiveMax = Math.max(1, this.retryConfig.maxAttempts);
|
|
684
779
|
const allowedRetries = effectiveMax - 1;
|
|
685
780
|
if (this.currentAttempt >= allowedRetries) {
|
|
686
|
-
debugSession("max attempts reached (%d), aborting", effectiveMax);
|
|
781
|
+
debugSession("[%s] max attempts reached (%d), aborting", this.streamName, effectiveMax);
|
|
687
782
|
const wrappedError = new error_js_1.S2Error({
|
|
688
783
|
message: `Max attempts (${effectiveMax}) exhausted: ${error.message}`,
|
|
689
784
|
status: error.status,
|
|
@@ -695,7 +790,7 @@ class RetryAppendSession {
|
|
|
695
790
|
// Perform recovery
|
|
696
791
|
this.consecutiveFailures++;
|
|
697
792
|
this.currentAttempt++;
|
|
698
|
-
debugSession("performing recovery (retry %d/%d)", this.currentAttempt, allowedRetries);
|
|
793
|
+
debugSession("[%s] performing recovery (retry %d/%d)", this.streamName, this.currentAttempt, allowedRetries);
|
|
699
794
|
await this.recover();
|
|
700
795
|
}
|
|
701
796
|
}
|
|
@@ -705,15 +800,16 @@ class RetryAppendSession {
|
|
|
705
800
|
* Returns either the settled result or a timeout indicator.
|
|
706
801
|
*
|
|
707
802
|
* Per-attempt ack timeout semantics:
|
|
708
|
-
* - The deadline is computed from the
|
|
709
|
-
*
|
|
710
|
-
*
|
|
803
|
+
* - The deadline is computed from the current attempt's start time using a
|
|
804
|
+
* monotonic clock (performance.now) to avoid issues with wall clock adjustments.
|
|
805
|
+
* - Each retry gets a fresh timeout window (attemptStartedMonotonicMs is reset
|
|
806
|
+
* during recovery).
|
|
711
807
|
* - If attempt start is missing (for backward compatibility), we measure
|
|
712
808
|
* from "now" with the full timeout window.
|
|
713
809
|
*/
|
|
714
810
|
async waitForHead(head) {
|
|
715
|
-
const
|
|
716
|
-
const deadline =
|
|
811
|
+
const attemptStart = head.attemptStartedMonotonicMs ?? performance.now();
|
|
812
|
+
const deadline = attemptStart + this.requestTimeoutMillis;
|
|
717
813
|
const remaining = Math.max(0, deadline - performance.now());
|
|
718
814
|
let timer;
|
|
719
815
|
const timeoutP = new Promise((resolve) => {
|
|
@@ -735,53 +831,52 @@ class RetryAppendSession {
|
|
|
735
831
|
* Recover from transient error: recreate session and resubmit all inflight entries.
|
|
736
832
|
*/
|
|
737
833
|
async recover() {
|
|
738
|
-
debugSession("starting recovery");
|
|
834
|
+
debugSession("[%s] starting recovery", this.streamName);
|
|
739
835
|
// Calculate backoff delay
|
|
740
|
-
const delay = calculateDelay(this.consecutiveFailures - 1, this.retryConfig.
|
|
741
|
-
debugSession("backing off for %dms", delay);
|
|
836
|
+
const delay = calculateDelay(this.consecutiveFailures - 1, this.retryConfig.minDelayMillis, this.retryConfig.maxDelayMillis);
|
|
837
|
+
debugSession("[%s] backing off for %dms", this.streamName, delay);
|
|
742
838
|
await sleep(delay);
|
|
743
839
|
// Teardown old session
|
|
744
840
|
if (this.session) {
|
|
745
841
|
try {
|
|
746
842
|
const closeResult = await this.session.close();
|
|
747
843
|
if (!closeResult.ok) {
|
|
748
|
-
debugSession("error closing old session during recovery: %s", closeResult.error.message);
|
|
844
|
+
debugSession("[%s] error closing old session during recovery: %s", this.streamName, closeResult.error.message);
|
|
749
845
|
}
|
|
750
846
|
}
|
|
751
847
|
catch (e) {
|
|
752
|
-
debugSession("exception closing old session: %s", e);
|
|
848
|
+
debugSession("[%s] exception closing old session: %s", this.streamName, e);
|
|
753
849
|
}
|
|
754
850
|
this.session = undefined;
|
|
755
851
|
}
|
|
756
852
|
// Create new session
|
|
757
853
|
await this.ensureSession();
|
|
758
854
|
if (!this.session) {
|
|
759
|
-
debugSession("failed to create new session during recovery");
|
|
855
|
+
debugSession("[%s] failed to create new session during recovery", this.streamName);
|
|
760
856
|
// Will retry on next pump iteration
|
|
761
857
|
return;
|
|
762
858
|
}
|
|
763
859
|
// Store session in local variable to help TypeScript type narrowing
|
|
764
860
|
const session = this.session;
|
|
765
861
|
// Resubmit all inflight entries (replace their innerPromise and reset attempt start)
|
|
766
|
-
debugSession("resubmitting %d inflight entries", this.inflight.length);
|
|
862
|
+
debugSession("[%s] resubmitting %d inflight entries", this.streamName, this.inflight.length);
|
|
767
863
|
for (const entry of this.inflight) {
|
|
768
864
|
// Attach .catch to superseded promise to avoid unhandled rejection
|
|
769
865
|
entry.innerPromise.catch(() => { });
|
|
770
866
|
// Create new promise from new session
|
|
771
|
-
|
|
772
|
-
entry.
|
|
867
|
+
const attemptStarted = performance.now();
|
|
868
|
+
entry.attemptStartedMonotonicMs = attemptStarted;
|
|
869
|
+
entry.innerPromise = session.submit(entry.input);
|
|
870
|
+
debugSession("[%s] resubmitted entry (%d records, %d bytes, match_seq_num=%s)", this.streamName, entry.expectedCount, entry.input.meteredBytes, entry.input.matchSeqNum ?? "none");
|
|
773
871
|
}
|
|
774
|
-
debugSession("recovery complete");
|
|
872
|
+
debugSession("[%s] recovery complete", this.streamName);
|
|
775
873
|
}
|
|
776
874
|
/**
|
|
777
875
|
* Check if append can be retried under noSideEffects policy.
|
|
778
876
|
* For appends, idempotency requires match_seq_num.
|
|
779
877
|
*/
|
|
780
878
|
isIdempotent(entry) {
|
|
781
|
-
|
|
782
|
-
if (!args)
|
|
783
|
-
return false;
|
|
784
|
-
return args.match_seq_num !== undefined;
|
|
879
|
+
return entry.input.matchSeqNum !== undefined;
|
|
785
880
|
}
|
|
786
881
|
/**
|
|
787
882
|
* Ensure session exists, creating it if necessary.
|
|
@@ -791,11 +886,13 @@ class RetryAppendSession {
|
|
|
791
886
|
return;
|
|
792
887
|
}
|
|
793
888
|
try {
|
|
889
|
+
debugSession("[%s] creating new transport session", this.streamName);
|
|
794
890
|
this.session = await this.generator(this.sessionOptions);
|
|
891
|
+
debugSession("[%s] transport session created", this.streamName);
|
|
795
892
|
}
|
|
796
893
|
catch (e) {
|
|
797
894
|
const error = (0, error_js_1.s2Error)(e);
|
|
798
|
-
debugSession("failed to create session: %s", error.message);
|
|
895
|
+
debugSession("[%s] failed to create session: %s", this.streamName, error.message);
|
|
799
896
|
// Don't set this.session - will retry later
|
|
800
897
|
}
|
|
801
898
|
}
|
|
@@ -806,10 +903,11 @@ class RetryAppendSession {
|
|
|
806
903
|
if (this.pumpStopped) {
|
|
807
904
|
return; // Already aborted
|
|
808
905
|
}
|
|
809
|
-
debugSession("aborting session: %s", error.message);
|
|
906
|
+
debugSession("[%s] aborting session: %s", this.streamName, error.message);
|
|
810
907
|
this.fatalError = error;
|
|
811
908
|
this.pumpStopped = true;
|
|
812
909
|
// Resolve all inflight entries with error
|
|
910
|
+
debugSession("[%s] rejecting %d inflight entries", this.streamName, this.inflight.length);
|
|
813
911
|
for (const entry of this.inflight) {
|
|
814
912
|
if (entry.maybeResolve) {
|
|
815
913
|
entry.maybeResolve((0, result_js_1.err)(error));
|
|
@@ -818,25 +916,27 @@ class RetryAppendSession {
|
|
|
818
916
|
this.inflight.length = 0;
|
|
819
917
|
this.queuedBytes = 0;
|
|
820
918
|
this.pendingBytes = 0;
|
|
919
|
+
this.pendingBatches = 0;
|
|
821
920
|
// Error the readable stream
|
|
822
921
|
try {
|
|
823
922
|
this.acksController?.error(error);
|
|
824
923
|
}
|
|
825
924
|
catch (e) {
|
|
826
|
-
debugSession("failed to error acks controller: %s", e);
|
|
925
|
+
debugSession("[%s] failed to error acks controller: %s", this.streamName, e);
|
|
827
926
|
}
|
|
828
|
-
// Wake capacity
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
this.capacityWaiter = undefined;
|
|
927
|
+
// Wake all capacity waiters to unblock any pending writers
|
|
928
|
+
for (const waiter of this.capacityWaiters) {
|
|
929
|
+
waiter.resolve();
|
|
832
930
|
}
|
|
931
|
+
this.capacityWaiters = [];
|
|
833
932
|
// Close inner session
|
|
834
933
|
if (this.session) {
|
|
934
|
+
debugSession("[%s] closing inner session", this.streamName);
|
|
835
935
|
try {
|
|
836
936
|
await this.session.close();
|
|
837
937
|
}
|
|
838
938
|
catch (e) {
|
|
839
|
-
debugSession("error closing session during abort: %s", e);
|
|
939
|
+
debugSession("[%s] error closing session during abort: %s", this.streamName, e);
|
|
840
940
|
}
|
|
841
941
|
this.session = undefined;
|
|
842
942
|
}
|
|
@@ -853,7 +953,7 @@ class RetryAppendSession {
|
|
|
853
953
|
}
|
|
854
954
|
return;
|
|
855
955
|
}
|
|
856
|
-
debugSession("close requested");
|
|
956
|
+
debugSession("[%s] close requested", this.streamName);
|
|
857
957
|
this.closing = true;
|
|
858
958
|
// Wake pump if it's sleeping so it can check closing flag
|
|
859
959
|
if (this.pumpWakeup) {
|
|
@@ -861,6 +961,7 @@ class RetryAppendSession {
|
|
|
861
961
|
}
|
|
862
962
|
// Wait for pump to stop (drains inflight queue, including through recovery)
|
|
863
963
|
if (this.pumpPromise) {
|
|
964
|
+
debugSession("[%s] [CLOSE] awaiting pump to drain inflight queue", this.streamName);
|
|
864
965
|
await this.pumpPromise;
|
|
865
966
|
}
|
|
866
967
|
// Close inner session
|
|
@@ -868,11 +969,11 @@ class RetryAppendSession {
|
|
|
868
969
|
try {
|
|
869
970
|
const result = await this.session.close();
|
|
870
971
|
if (!result.ok) {
|
|
871
|
-
debugSession("error closing inner session: %s", result.error.message);
|
|
972
|
+
debugSession("[%s] error closing inner session: %s", this.streamName, result.error.message);
|
|
872
973
|
}
|
|
873
974
|
}
|
|
874
975
|
catch (e) {
|
|
875
|
-
debugSession("exception closing inner session: %s", e);
|
|
976
|
+
debugSession("[%s] exception closing inner session: %s", this.streamName, e);
|
|
876
977
|
}
|
|
877
978
|
this.session = undefined;
|
|
878
979
|
}
|
|
@@ -881,14 +982,14 @@ class RetryAppendSession {
|
|
|
881
982
|
this.acksController?.close();
|
|
882
983
|
}
|
|
883
984
|
catch (e) {
|
|
884
|
-
debugSession("error closing acks controller: %s", e);
|
|
985
|
+
debugSession("[%s] error closing acks controller: %s", this.streamName, e);
|
|
885
986
|
}
|
|
886
987
|
this.closed = true;
|
|
887
988
|
// If fatal error occurred, throw it
|
|
888
989
|
if (this.fatalError) {
|
|
889
990
|
throw this.fatalError;
|
|
890
991
|
}
|
|
891
|
-
debugSession("close complete");
|
|
992
|
+
debugSession("[%s] close complete", this.streamName);
|
|
892
993
|
}
|
|
893
994
|
async [Symbol.asyncDispose]() {
|
|
894
995
|
await this.close();
|