@s2-dev/streamstore 0.20.0 → 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.
Files changed (221) hide show
  1. package/LICENSE +21 -201
  2. package/README.md +60 -10
  3. package/dist/cjs/accessTokens.d.ts +27 -14
  4. package/dist/cjs/accessTokens.d.ts.map +1 -1
  5. package/dist/cjs/accessTokens.js +72 -8
  6. package/dist/cjs/accessTokens.js.map +1 -1
  7. package/dist/cjs/basins.d.ts +29 -19
  8. package/dist/cjs/basins.d.ts.map +1 -1
  9. package/dist/cjs/basins.js +119 -9
  10. package/dist/cjs/basins.js.map +1 -1
  11. package/dist/cjs/batch-transform.d.ts +12 -16
  12. package/dist/cjs/batch-transform.d.ts.map +1 -1
  13. package/dist/cjs/batch-transform.js +17 -21
  14. package/dist/cjs/batch-transform.js.map +1 -1
  15. package/dist/cjs/common.d.ts +31 -24
  16. package/dist/cjs/common.d.ts.map +1 -1
  17. package/dist/cjs/common.js +22 -0
  18. package/dist/cjs/common.js.map +1 -1
  19. package/dist/cjs/endpoints.d.ts +63 -0
  20. package/dist/cjs/endpoints.d.ts.map +1 -0
  21. package/dist/cjs/endpoints.js +120 -0
  22. package/dist/cjs/endpoints.js.map +1 -0
  23. package/dist/cjs/error.d.ts.map +1 -1
  24. package/dist/cjs/error.js +11 -0
  25. package/dist/cjs/error.js.map +1 -1
  26. package/dist/cjs/generated/types.gen.d.ts +11 -20
  27. package/dist/cjs/generated/types.gen.d.ts.map +1 -1
  28. package/dist/cjs/index.d.ts +30 -46
  29. package/dist/cjs/index.d.ts.map +1 -1
  30. package/dist/cjs/index.js +50 -26
  31. package/dist/cjs/index.js.map +1 -1
  32. package/dist/cjs/internal/case-transform.d.ts +59 -0
  33. package/dist/cjs/internal/case-transform.d.ts.map +1 -0
  34. package/dist/cjs/internal/case-transform.js +80 -0
  35. package/dist/cjs/internal/case-transform.js.map +1 -0
  36. package/dist/cjs/internal/mappers.d.ts +51 -0
  37. package/dist/cjs/internal/mappers.d.ts.map +1 -0
  38. package/dist/cjs/internal/mappers.js +225 -0
  39. package/dist/cjs/internal/mappers.js.map +1 -0
  40. package/dist/cjs/internal/sdk-types.d.ts +127 -0
  41. package/dist/cjs/internal/sdk-types.d.ts.map +1 -0
  42. package/dist/cjs/internal/sdk-types.js +9 -0
  43. package/dist/cjs/internal/sdk-types.js.map +1 -0
  44. package/dist/cjs/lib/base64.d.ts +8 -0
  45. package/dist/cjs/lib/base64.d.ts.map +1 -1
  46. package/dist/cjs/lib/base64.js +32 -12
  47. package/dist/cjs/lib/base64.js.map +1 -1
  48. package/dist/cjs/lib/event-stream.d.ts.map +1 -1
  49. package/dist/cjs/lib/event-stream.js +2 -1
  50. package/dist/cjs/lib/event-stream.js.map +1 -1
  51. package/dist/cjs/lib/paginate.d.ts +57 -0
  52. package/dist/cjs/lib/paginate.d.ts.map +1 -0
  53. package/dist/cjs/lib/paginate.js +51 -0
  54. package/dist/cjs/lib/paginate.js.map +1 -0
  55. package/dist/cjs/lib/result.d.ts +1 -1
  56. package/dist/cjs/lib/result.d.ts.map +1 -1
  57. package/dist/cjs/lib/retry.d.ts +47 -31
  58. package/dist/cjs/lib/retry.d.ts.map +1 -1
  59. package/dist/cjs/lib/retry.js +302 -201
  60. package/dist/cjs/lib/retry.js.map +1 -1
  61. package/dist/cjs/lib/stream/runtime.d.ts +1 -1
  62. package/dist/cjs/lib/stream/transport/fetch/index.d.ts +7 -9
  63. package/dist/cjs/lib/stream/transport/fetch/index.d.ts.map +1 -1
  64. package/dist/cjs/lib/stream/transport/fetch/index.js +38 -39
  65. package/dist/cjs/lib/stream/transport/fetch/index.js.map +1 -1
  66. package/dist/cjs/lib/stream/transport/fetch/shared.d.ts +7 -2
  67. package/dist/cjs/lib/stream/transport/fetch/shared.d.ts.map +1 -1
  68. package/dist/cjs/lib/stream/transport/fetch/shared.js +56 -110
  69. package/dist/cjs/lib/stream/transport/fetch/shared.js.map +1 -1
  70. package/dist/cjs/lib/stream/transport/proto.d.ts +9 -0
  71. package/dist/cjs/lib/stream/transport/proto.d.ts.map +1 -0
  72. package/dist/cjs/lib/stream/transport/proto.js +118 -0
  73. package/dist/cjs/lib/stream/transport/proto.js.map +1 -0
  74. package/dist/cjs/lib/stream/transport/s2s/index.d.ts +3 -3
  75. package/dist/cjs/lib/stream/transport/s2s/index.d.ts.map +1 -1
  76. package/dist/cjs/lib/stream/transport/s2s/index.js +115 -82
  77. package/dist/cjs/lib/stream/transport/s2s/index.js.map +1 -1
  78. package/dist/cjs/lib/stream/types.d.ts +81 -36
  79. package/dist/cjs/lib/stream/types.d.ts.map +1 -1
  80. package/dist/cjs/lib/stream/types.js +18 -0
  81. package/dist/cjs/lib/stream/types.js.map +1 -1
  82. package/dist/cjs/metrics.d.ts +18 -17
  83. package/dist/cjs/metrics.d.ts.map +1 -1
  84. package/dist/cjs/metrics.js +67 -12
  85. package/dist/cjs/metrics.js.map +1 -1
  86. package/dist/cjs/producer.d.ts +82 -0
  87. package/dist/cjs/producer.d.ts.map +1 -0
  88. package/dist/cjs/producer.js +305 -0
  89. package/dist/cjs/producer.js.map +1 -0
  90. package/dist/cjs/s2.d.ts +1 -2
  91. package/dist/cjs/s2.d.ts.map +1 -1
  92. package/dist/cjs/s2.js +9 -9
  93. package/dist/cjs/s2.js.map +1 -1
  94. package/dist/cjs/stream.d.ts +26 -12
  95. package/dist/cjs/stream.d.ts.map +1 -1
  96. package/dist/cjs/stream.js +77 -13
  97. package/dist/cjs/stream.js.map +1 -1
  98. package/dist/cjs/streams.d.ts +29 -19
  99. package/dist/cjs/streams.d.ts.map +1 -1
  100. package/dist/cjs/streams.js +120 -9
  101. package/dist/cjs/streams.js.map +1 -1
  102. package/dist/cjs/types.d.ts +624 -0
  103. package/dist/cjs/types.d.ts.map +1 -0
  104. package/dist/cjs/types.js +129 -0
  105. package/dist/cjs/types.js.map +1 -0
  106. package/dist/cjs/utils.d.ts +1 -22
  107. package/dist/cjs/utils.d.ts.map +1 -1
  108. package/dist/cjs/utils.js +0 -42
  109. package/dist/cjs/utils.js.map +1 -1
  110. package/dist/cjs/version.d.ts +1 -1
  111. package/dist/cjs/version.js +1 -1
  112. package/dist/esm/accessTokens.d.ts +27 -14
  113. package/dist/esm/accessTokens.d.ts.map +1 -1
  114. package/dist/esm/accessTokens.js +73 -9
  115. package/dist/esm/accessTokens.js.map +1 -1
  116. package/dist/esm/basins.d.ts +29 -19
  117. package/dist/esm/basins.d.ts.map +1 -1
  118. package/dist/esm/basins.js +119 -9
  119. package/dist/esm/basins.js.map +1 -1
  120. package/dist/esm/batch-transform.d.ts +12 -16
  121. package/dist/esm/batch-transform.d.ts.map +1 -1
  122. package/dist/esm/batch-transform.js +18 -22
  123. package/dist/esm/batch-transform.js.map +1 -1
  124. package/dist/esm/common.d.ts +31 -24
  125. package/dist/esm/common.d.ts.map +1 -1
  126. package/dist/esm/common.js +20 -1
  127. package/dist/esm/common.js.map +1 -1
  128. package/dist/esm/endpoints.d.ts +63 -0
  129. package/dist/esm/endpoints.d.ts.map +1 -0
  130. package/dist/esm/endpoints.js +115 -0
  131. package/dist/esm/endpoints.js.map +1 -0
  132. package/dist/esm/error.d.ts.map +1 -1
  133. package/dist/esm/error.js +11 -0
  134. package/dist/esm/error.js.map +1 -1
  135. package/dist/esm/generated/types.gen.d.ts +11 -20
  136. package/dist/esm/generated/types.gen.d.ts.map +1 -1
  137. package/dist/esm/index.d.ts +30 -46
  138. package/dist/esm/index.d.ts.map +1 -1
  139. package/dist/esm/index.js +33 -19
  140. package/dist/esm/index.js.map +1 -1
  141. package/dist/esm/internal/case-transform.d.ts +59 -0
  142. package/dist/esm/internal/case-transform.d.ts.map +1 -0
  143. package/dist/esm/internal/case-transform.js +76 -0
  144. package/dist/esm/internal/case-transform.js.map +1 -0
  145. package/dist/esm/internal/mappers.d.ts +51 -0
  146. package/dist/esm/internal/mappers.d.ts.map +1 -0
  147. package/dist/esm/internal/mappers.js +218 -0
  148. package/dist/esm/internal/mappers.js.map +1 -0
  149. package/dist/esm/internal/sdk-types.d.ts +127 -0
  150. package/dist/esm/internal/sdk-types.d.ts.map +1 -0
  151. package/dist/esm/internal/sdk-types.js +8 -0
  152. package/dist/esm/internal/sdk-types.js.map +1 -0
  153. package/dist/esm/lib/base64.d.ts +8 -0
  154. package/dist/esm/lib/base64.d.ts.map +1 -1
  155. package/dist/esm/lib/base64.js +30 -11
  156. package/dist/esm/lib/base64.js.map +1 -1
  157. package/dist/esm/lib/event-stream.d.ts.map +1 -1
  158. package/dist/esm/lib/event-stream.js +2 -1
  159. package/dist/esm/lib/event-stream.js.map +1 -1
  160. package/dist/esm/lib/paginate.d.ts +57 -0
  161. package/dist/esm/lib/paginate.d.ts.map +1 -0
  162. package/dist/esm/lib/paginate.js +48 -0
  163. package/dist/esm/lib/paginate.js.map +1 -0
  164. package/dist/esm/lib/result.d.ts +1 -1
  165. package/dist/esm/lib/result.d.ts.map +1 -1
  166. package/dist/esm/lib/retry.d.ts +47 -31
  167. package/dist/esm/lib/retry.d.ts.map +1 -1
  168. package/dist/esm/lib/retry.js +303 -201
  169. package/dist/esm/lib/retry.js.map +1 -1
  170. package/dist/esm/lib/stream/runtime.d.ts +1 -1
  171. package/dist/esm/lib/stream/transport/fetch/index.d.ts +7 -9
  172. package/dist/esm/lib/stream/transport/fetch/index.d.ts.map +1 -1
  173. package/dist/esm/lib/stream/transport/fetch/index.js +40 -41
  174. package/dist/esm/lib/stream/transport/fetch/index.js.map +1 -1
  175. package/dist/esm/lib/stream/transport/fetch/shared.d.ts +7 -2
  176. package/dist/esm/lib/stream/transport/fetch/shared.d.ts.map +1 -1
  177. package/dist/esm/lib/stream/transport/fetch/shared.js +58 -112
  178. package/dist/esm/lib/stream/transport/fetch/shared.js.map +1 -1
  179. package/dist/esm/lib/stream/transport/proto.d.ts +9 -0
  180. package/dist/esm/lib/stream/transport/proto.d.ts.map +1 -0
  181. package/dist/esm/lib/stream/transport/proto.js +110 -0
  182. package/dist/esm/lib/stream/transport/proto.js.map +1 -0
  183. package/dist/esm/lib/stream/transport/s2s/index.d.ts +3 -3
  184. package/dist/esm/lib/stream/transport/s2s/index.d.ts.map +1 -1
  185. package/dist/esm/lib/stream/transport/s2s/index.js +116 -82
  186. package/dist/esm/lib/stream/transport/s2s/index.js.map +1 -1
  187. package/dist/esm/lib/stream/types.d.ts +81 -36
  188. package/dist/esm/lib/stream/types.d.ts.map +1 -1
  189. package/dist/esm/lib/stream/types.js +17 -1
  190. package/dist/esm/lib/stream/types.js.map +1 -1
  191. package/dist/esm/metrics.d.ts +18 -17
  192. package/dist/esm/metrics.d.ts.map +1 -1
  193. package/dist/esm/metrics.js +66 -12
  194. package/dist/esm/metrics.js.map +1 -1
  195. package/dist/esm/producer.d.ts +82 -0
  196. package/dist/esm/producer.d.ts.map +1 -0
  197. package/dist/esm/producer.js +300 -0
  198. package/dist/esm/producer.js.map +1 -0
  199. package/dist/esm/s2.d.ts +1 -2
  200. package/dist/esm/s2.d.ts.map +1 -1
  201. package/dist/esm/s2.js +9 -9
  202. package/dist/esm/s2.js.map +1 -1
  203. package/dist/esm/stream.d.ts +26 -12
  204. package/dist/esm/stream.d.ts.map +1 -1
  205. package/dist/esm/stream.js +79 -15
  206. package/dist/esm/stream.js.map +1 -1
  207. package/dist/esm/streams.d.ts +29 -19
  208. package/dist/esm/streams.d.ts.map +1 -1
  209. package/dist/esm/streams.js +120 -9
  210. package/dist/esm/streams.js.map +1 -1
  211. package/dist/esm/types.d.ts +624 -0
  212. package/dist/esm/types.d.ts.map +1 -0
  213. package/dist/esm/types.js +126 -0
  214. package/dist/esm/types.js.map +1 -0
  215. package/dist/esm/utils.d.ts +1 -22
  216. package/dist/esm/utils.d.ts.map +1 -1
  217. package/dist/esm/utils.js +0 -41
  218. package/dist/esm/utils.js.map +1 -1
  219. package/dist/esm/version.d.ts +1 -1
  220. package/dist/esm/version.js +1 -1
  221. package/package.json +4 -3
@@ -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
- retryBackoffDurationMillis: 100,
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 fixed backoff
50
- * with jitter. The `attempt` parameter is currently ignored to keep a
51
- * constant base delay per attempt.
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, baseDelayMillis) {
54
- // Apply ±50% jitter around the base delay
55
- const jitterRange = 0.5; // 50% up or down
56
- const factor = 1 + (Math.random() * 2 - 1) * jitterRange; // [0.5, 1.5]
57
- const delay = Math.max(0, baseDelayMillis * factor);
58
- return Math.floor(delay);
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.retryBackoffDurationMillis);
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 = err instanceof error_js_1.S2Error
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.retryBackoffDurationMillis);
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 = err instanceof error_js_1.S2Error
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.retryBackoffDurationMillis);
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.retryBackoffDurationMillis);
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?.code !== "ERR_INVALID_STATE") {
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 fn = ReadableStream.prototype[Symbol.asyncIterator];
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?.code !== "ERR_INVALID_STATE")
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?.code !== "ERR_INVALID_STATE")
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 DEFAULT_MAX_INFLIGHT_BYTES = 10 * 1024 * 1024; // 10 MiB default
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
- capacityWaiter; // Single waiter (WritableStream writer lock)
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
- this.maxQueuedBytes =
391
- this.sessionOptions?.maxInflightBytes ?? DEFAULT_MAX_INFLIGHT_BYTES;
392
- this.maxInflightBatches = this.sessionOptions?.maxInflightBatches;
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
- const recordsArray = Array.isArray(chunk.records)
401
- ? chunk.records
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
- // Wait for capacity (backpressure for writable only)
409
- await this.waitForCapacity(batchMeteredSize);
410
- const { records: _records, ...rest } = chunk;
411
- const args = rest;
412
- args.precalculatedSize = batchMeteredSize;
413
- // Move reserved bytes to queued bytes accounting before submission
414
- this.pendingBytes = Math.max(0, this.pendingBytes - batchMeteredSize);
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
- * Submit an append request. Returns a promise that resolves with the ack.
435
- * This method does not block on capacity (only writable.write() does).
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 submit(records, args) {
438
- const recordsArray = Array.isArray(records) ? records : [records];
439
- // Calculate metered size if not provided
440
- let batchMeteredSize = args?.precalculatedSize ?? 0;
441
- if (batchMeteredSize === 0) {
442
- for (const record of recordsArray) {
443
- batchMeteredSize += (0, utils_js_1.meteredBytes)(record);
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
- const result = await this.submitInternal(recordsArray, args, batchMeteredSize);
447
- // Convert discriminated union back to throw pattern for public API
448
- if (result.ok) {
449
- return result.value;
450
- }
451
- else {
452
- throw result.error;
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(records, args, batchMeteredSize) {
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
- records,
473
- args,
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, hasWaiter=%s", bytes, this.queuedBytes, this.queuedBytes - bytes, this.pendingBytes, Math.max(0, this.pendingBytes - bytes), !!this.capacityWaiter);
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
- // Wake single waiter
530
- const waiter = this.capacityWaiter;
531
- if (waiter) {
532
- debugSession("[CAPACITY] waking waiter");
533
- this.capacityWaiter = undefined;
534
- waiter();
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 (this.closing && this.inflight.length === 0) {
563
- debugSession("[PUMP] closing and queue empty, stopping");
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, sleeping 10ms");
570
- // Use interruptible sleep - can be woken by new submissions
571
- await Promise.race([
572
- sleep(10),
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
- debugSession("[PUMP] session creation failed, sleeping 100ms");
589
- await sleep(100);
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
- entry.attemptStartedMonotonicMs = performance.now();
597
- entry.innerPromise = this.session.submit(entry.records, entry.args);
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 - fatal (per-attempt)
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
- await this.abort(error);
617
- return;
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", { 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 = Number(ack.end.seq_num) - Number(ack.start.seq_num);
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 = BigInt(this._lastAckedPosition.end.seq_num);
636
- const currentEnd = BigInt(ack.end.seq_num);
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 most recent (re)submit attempt using
709
- * a monotonic clock (performance.now) to avoid issues with wall clock
710
- * adjustments.
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 startMono = head.attemptStartedMonotonicMs ?? performance.now();
716
- const deadline = startMono + this.requestTimeoutMillis;
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.retryBackoffDurationMillis);
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
- entry.attemptStartedMonotonicMs = performance.now();
772
- entry.innerPromise = session.submit(entry.records, entry.args);
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
- const args = entry.args;
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 waiter to unblock any pending writer
829
- if (this.capacityWaiter) {
830
- this.capacityWaiter();
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();