@doist/comms-sdk 3.0.1 → 3.0.2

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.
@@ -79,22 +79,30 @@ function describeTransportFailure(error, attempts) {
79
79
  }
80
80
  function describeCauseChain(error) {
81
81
  const descriptions = [];
82
- const seen = new Set();
83
- let current = error?.cause;
84
- while (current instanceof Error &&
85
- !seen.has(current) &&
86
- descriptions.length < CAUSE_CHAIN_LIMIT) {
87
- seen.add(current);
88
- const description = describeCause(current);
82
+ for (const cause of causeChain(error)) {
83
+ const description = describeCause(cause);
89
84
  if (description) {
90
85
  descriptions.push(description);
91
86
  }
92
- // A failed connect can carry one error per address it tried; the first
93
- // one is representative and the rest are the same failure repeated.
87
+ }
88
+ return descriptions.length > 0 ? descriptions.join(' <- ') : undefined;
89
+ }
90
+ /**
91
+ * Walks an error's `cause` chain, stopping at {@link CAUSE_CHAIN_LIMIT} and on
92
+ * a cycle. A failed connect can carry one error per address it tried; the first
93
+ * is representative and the rest repeat the same failure.
94
+ */
95
+ function* causeChain(error) {
96
+ const seen = new Set();
97
+ let current = error?.cause;
98
+ let depth = 0;
99
+ while (current instanceof Error && !seen.has(current) && depth < CAUSE_CHAIN_LIMIT) {
100
+ seen.add(current);
101
+ depth++;
102
+ yield current;
94
103
  const aggregated = current instanceof AggregateError ? current.errors[0] : undefined;
95
104
  current = aggregated ?? current.cause;
96
105
  }
97
- return descriptions.length > 0 ? descriptions.join(' <- ') : undefined;
98
106
  }
99
107
  function describeCause(error) {
100
108
  const code = error.code;
@@ -134,13 +142,60 @@ function createTimeoutError(timeoutMs) {
134
142
  error.name = timeoutErrorName;
135
143
  return error;
136
144
  }
145
+ /**
146
+ * Transport-level failure codes worth another attempt: the request never got an
147
+ * answer, so nothing about the response argues against retrying. `UND_ERR_SOCKET`
148
+ * covers the HTTP/2 `GOAWAY` an edge proxy sends when it retires a pooled
149
+ * connection, which is otherwise indistinguishable from any other `fetch failed`.
150
+ */
151
+ const NETWORK_ERROR_CODES = new Set([
152
+ 'ECONNREFUSED',
153
+ 'ECONNRESET',
154
+ 'EAI_AGAIN',
155
+ 'ENOTFOUND',
156
+ 'EPIPE',
157
+ 'ETIMEDOUT',
158
+ 'UND_ERR_SOCKET',
159
+ ]);
137
160
  function isNetworkError(error) {
138
161
  return (error.name === 'TypeError' ||
139
162
  error.name === timeoutErrorName ||
140
- error.message.toLowerCase().includes('network'));
163
+ error.message.toLowerCase().includes('network') ||
164
+ hasNetworkErrorCode(error));
165
+ }
166
+ function hasNetworkErrorCode(error) {
167
+ // The code can be on the error itself — a `customFetch` is free to reject
168
+ // with one directly — or on anything `fetch` wrapped to produce it.
169
+ if (hasRetryableCode(error)) {
170
+ return true;
171
+ }
172
+ for (const cause of causeChain(error)) {
173
+ if (hasRetryableCode(cause)) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
141
178
  }
179
+ function hasRetryableCode(error) {
180
+ const code = error.code;
181
+ return typeof code === 'string' && NETWORK_ERROR_CODES.has(code);
182
+ }
183
+ /** First retry delay, doubling by {@link RETRY_DELAY_FACTOR} per attempt. */
184
+ const RETRY_BASE_DELAY_MS = 100;
185
+ const RETRY_DELAY_FACTOR = 4;
186
+ const RETRY_MAX_DELAY_MS = 2000;
187
+ /**
188
+ * How long to wait before retry `retryCount` (1-based).
189
+ *
190
+ * A retired connection takes a moment to be torn down and replaced, so retrying
191
+ * immediately just re-dispatches onto the socket that is going away and burns
192
+ * the whole retry budget inside a second. The delay grows so the later attempts
193
+ * land on a fresh connection, and carries jitter because one retirement hits
194
+ * every pooled connection at once: without it, every caller reconnects in step.
195
+ */
142
196
  function getRetryDelay(retryCount) {
143
- return retryCount === 1 ? 0 : 500;
197
+ const delay = Math.min(RETRY_BASE_DELAY_MS * RETRY_DELAY_FACTOR ** (retryCount - 1), RETRY_MAX_DELAY_MS);
198
+ return delay / 2 + Math.random() * (delay / 2);
144
199
  }
145
200
  function createTimeoutSignal(timeoutMs, existingSignal) {
146
201
  const controller = new AbortController();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/comms-sdk",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "A TypeScript wrapper for the Comms REST API.",
5
5
  "author": "Doist developers",
6
6
  "homepage": "https://doist.github.io/comms-sdk-typescript/",