@upyo/retry 0.5.0-dev.195 → 0.5.0-dev.199

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/dist/index.cjs CHANGED
@@ -113,6 +113,7 @@ var RetryTransport = class {
113
113
  * @param transport The transport to wrap.
114
114
  * @param config Retry configuration.
115
115
  * @throws {RangeError} If retry configuration is invalid.
116
+ * @since 0.5.0
116
117
  */
117
118
  constructor(transport, config = {}) {
118
119
  this.wrappedTransport = transport;
@@ -126,25 +127,16 @@ var RetryTransport = class {
126
127
  * @param options Optional transport options.
127
128
  * @returns The final delivery receipt.
128
129
  * @throws {DOMException} If the operation is aborted.
130
+ * @since 0.5.0
129
131
  */
130
132
  async send(message, options) {
131
133
  options?.signal?.throwIfAborted();
132
134
  let lastThrownError;
133
135
  for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) {
134
136
  options?.signal?.throwIfAborted();
137
+ let receipt;
135
138
  try {
136
- const receipt = await this.wrappedTransport.send(message, options);
137
- if (receipt.successful) return this.withSuccessMetadata(receipt, attempt);
138
- const failedReceipt = this.withFailureMetadata(receipt, attempt);
139
- if (attempt >= this.config.maxAttempts || !this.shouldRetryReceipt(failedReceipt)) return failedReceipt;
140
- await this.waitBeforeRetry({
141
- attempt,
142
- nextAttempt: attempt + 1,
143
- maxAttempts: this.config.maxAttempts,
144
- delayMilliseconds: this.calculateDelay(attempt, failedReceipt),
145
- receipt: failedReceipt,
146
- reason: "retry"
147
- }, options?.signal);
139
+ receipt = await this.wrappedTransport.send(message, options);
148
140
  } catch (error) {
149
141
  options?.signal?.throwIfAborted();
150
142
  lastThrownError = error;
@@ -157,7 +149,20 @@ var RetryTransport = class {
157
149
  error,
158
150
  reason: "retry"
159
151
  }, options?.signal);
152
+ continue;
160
153
  }
154
+ options?.signal?.throwIfAborted();
155
+ if (receipt.successful) return this.withSuccessMetadata(receipt, attempt);
156
+ const failedReceipt = this.withFailureMetadata(receipt, attempt);
157
+ if (attempt >= this.config.maxAttempts || !this.shouldRetryReceipt(failedReceipt)) return failedReceipt;
158
+ await this.waitBeforeRetry({
159
+ attempt,
160
+ nextAttempt: attempt + 1,
161
+ maxAttempts: this.config.maxAttempts,
162
+ delayMilliseconds: this.calculateDelay(attempt, failedReceipt),
163
+ receipt: failedReceipt,
164
+ reason: "retry"
165
+ }, options?.signal);
161
166
  }
162
167
  return this.createThrownFailure(lastThrownError, this.config.maxAttempts);
163
168
  }
@@ -170,6 +175,7 @@ var RetryTransport = class {
170
175
  * @param options Optional transport options.
171
176
  * @returns An async iterable of receipts.
172
177
  * @throws {DOMException} If the operation is aborted.
178
+ * @since 0.5.0
173
179
  */
174
180
  async *sendMany(messages, options) {
175
181
  const iterator = toAsyncIterator(messages);
@@ -193,12 +199,16 @@ var RetryTransport = class {
193
199
  if (inputDone) return;
194
200
  combinedSignal.signal.throwIfAborted();
195
201
  pullingInput = true;
196
- let next;
202
+ let inputPull;
197
203
  try {
198
- next = await iterator.next();
199
- } finally {
200
- pullingInput = false;
204
+ inputPull = Promise.resolve(iterator.next());
205
+ } catch (error) {
206
+ inputPull = Promise.reject(error);
201
207
  }
208
+ inputPull = inputPull.finally(() => {
209
+ pullingInput = false;
210
+ });
211
+ const next = await raceWithAbort(inputPull, combinedSignal.signal);
202
212
  if (closed) return;
203
213
  if (next.done) {
204
214
  inputDone = true;
@@ -219,7 +229,7 @@ var RetryTransport = class {
219
229
  inFlight.set(index, promise);
220
230
  };
221
231
  const startLaunch = () => {
222
- if (launchPromise != null || inputDone || inFlight.size >= this.config.sendMany.maxConcurrent) return;
232
+ if (launchPromise != null || inputDone || hasLaunchError || inFlight.size >= this.config.sendMany.maxConcurrent) return;
223
233
  launchPromise = launchNext().catch((error) => {
224
234
  launchError = error;
225
235
  hasLaunchError = true;
@@ -239,7 +249,7 @@ var RetryTransport = class {
239
249
  nextYieldIndex++;
240
250
  startLaunch();
241
251
  }
242
- if (hasLaunchError) throw launchError;
252
+ if (hasLaunchError && inFlight.size <= 0) throw launchError;
243
253
  startLaunch();
244
254
  if (inFlight.size <= 0 && launchPromise == null) break;
245
255
  const launch = launchPromise?.then(() => ({ launched: true }));
@@ -255,13 +265,10 @@ var RetryTransport = class {
255
265
  controller.abort(new DOMException("The operation was aborted.", "AbortError"));
256
266
  launchPromise?.catch(() => {});
257
267
  try {
258
- const closeIterator = async () => {
259
- if (!inputDone) await iterator.return?.();
260
- };
261
- if (pullingInput) launchPromise?.finally(() => {
262
- closeIterator().catch(() => {});
263
- });
264
- else await closeIterator();
268
+ const closeIterator = () => inputDone ? void 0 : iterator.return?.();
269
+ const closePromise = closeIterator();
270
+ if (pullingInput) closePromise?.catch(() => {});
271
+ else await closePromise;
265
272
  } finally {
266
273
  combinedSignal.cleanup();
267
274
  }
@@ -270,6 +277,8 @@ var RetryTransport = class {
270
277
  /**
271
278
  * Disposes the wrapped transport when it supports disposal.
272
279
  *
280
+ * @throws {unknown} If the wrapped transport's async or sync disposal hook
281
+ * throws or rejects.
273
282
  * @since 0.5.0
274
283
  */
275
284
  async [Symbol.asyncDispose]() {
@@ -324,7 +333,7 @@ var RetryTransport = class {
324
333
  }
325
334
  calculateDelay(attempt, failure) {
326
335
  const retryAfterMilliseconds = getRetryAfterMilliseconds(failure);
327
- const cappedRetryAfter = retryAfterMilliseconds == null ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
336
+ const cappedRetryAfter = retryAfterMilliseconds == null || !Number.isFinite(retryAfterMilliseconds) || retryAfterMilliseconds <= 0 ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
328
337
  if (cappedRetryAfter != null) return cappedRetryAfter;
329
338
  const computedDelay = Math.min(this.config.backoff.baseDelayMilliseconds * Math.pow(this.config.backoff.factor, attempt - 1), this.config.backoff.maxDelayMilliseconds);
330
339
  if (this.config.jitter === false || this.config.jitter === "none") return computedDelay;
@@ -387,8 +396,40 @@ function getRetryAfterMilliseconds(failure) {
387
396
  if (isReceiptError(failure)) return failure.retryAfterMilliseconds;
388
397
  return failure.errors?.find((error) => error.retryAfterMilliseconds != null)?.retryAfterMilliseconds;
389
398
  }
390
- async function* toAsyncIterator(values) {
391
- for await (const value of values) yield value;
399
+ function toAsyncIterator(values) {
400
+ if (Symbol.asyncIterator in values) return values[Symbol.asyncIterator]();
401
+ const iterator = values[Symbol.iterator]();
402
+ return {
403
+ async next() {
404
+ return await Promise.resolve(iterator.next());
405
+ },
406
+ async return(value) {
407
+ return await Promise.resolve(iterator.return?.(value) ?? {
408
+ done: true,
409
+ value
410
+ });
411
+ }
412
+ };
413
+ }
414
+ function raceWithAbort(promise, signal) {
415
+ if (signal.aborted) return Promise.reject(signal.reason);
416
+ return new Promise((resolve, reject) => {
417
+ const abort = () => {
418
+ cleanup();
419
+ reject(signal.reason);
420
+ };
421
+ const cleanup = () => {
422
+ signal.removeEventListener("abort", abort);
423
+ };
424
+ signal.addEventListener("abort", abort, { once: true });
425
+ promise.then((value) => {
426
+ cleanup();
427
+ resolve(value);
428
+ }, (error) => {
429
+ cleanup();
430
+ reject(error);
431
+ });
432
+ });
392
433
  }
393
434
  function toReceiptError(value) {
394
435
  return isReceiptError(value) ? value : void 0;
package/dist/index.d.cts CHANGED
@@ -207,6 +207,7 @@ declare class RetryTransport<TProviderId extends string = string> implements Tra
207
207
  * @param transport The transport to wrap.
208
208
  * @param config Retry configuration.
209
209
  * @throws {RangeError} If retry configuration is invalid.
210
+ * @since 0.5.0
210
211
  */
211
212
  constructor(transport: Transport<TProviderId>, config?: RetryConfig<TProviderId>);
212
213
  /**
@@ -216,6 +217,7 @@ declare class RetryTransport<TProviderId extends string = string> implements Tra
216
217
  * @param options Optional transport options.
217
218
  * @returns The final delivery receipt.
218
219
  * @throws {DOMException} If the operation is aborted.
220
+ * @since 0.5.0
219
221
  */
220
222
  send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId>>;
221
223
  /**
@@ -227,11 +229,14 @@ declare class RetryTransport<TProviderId extends string = string> implements Tra
227
229
  * @param options Optional transport options.
228
230
  * @returns An async iterable of receipts.
229
231
  * @throws {DOMException} If the operation is aborted.
232
+ * @since 0.5.0
230
233
  */
231
234
  sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
232
235
  /**
233
236
  * Disposes the wrapped transport when it supports disposal.
234
237
  *
238
+ * @throws {unknown} If the wrapped transport's async or sync disposal hook
239
+ * throws or rejects.
235
240
  * @since 0.5.0
236
241
  */
237
242
  [Symbol.asyncDispose](): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -207,6 +207,7 @@ declare class RetryTransport<TProviderId extends string = string> implements Tra
207
207
  * @param transport The transport to wrap.
208
208
  * @param config Retry configuration.
209
209
  * @throws {RangeError} If retry configuration is invalid.
210
+ * @since 0.5.0
210
211
  */
211
212
  constructor(transport: Transport<TProviderId>, config?: RetryConfig<TProviderId>);
212
213
  /**
@@ -216,6 +217,7 @@ declare class RetryTransport<TProviderId extends string = string> implements Tra
216
217
  * @param options Optional transport options.
217
218
  * @returns The final delivery receipt.
218
219
  * @throws {DOMException} If the operation is aborted.
220
+ * @since 0.5.0
219
221
  */
220
222
  send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId>>;
221
223
  /**
@@ -227,11 +229,14 @@ declare class RetryTransport<TProviderId extends string = string> implements Tra
227
229
  * @param options Optional transport options.
228
230
  * @returns An async iterable of receipts.
229
231
  * @throws {DOMException} If the operation is aborted.
232
+ * @since 0.5.0
230
233
  */
231
234
  sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
232
235
  /**
233
236
  * Disposes the wrapped transport when it supports disposal.
234
237
  *
238
+ * @throws {unknown} If the wrapped transport's async or sync disposal hook
239
+ * throws or rejects.
235
240
  * @since 0.5.0
236
241
  */
237
242
  [Symbol.asyncDispose](): Promise<void>;
package/dist/index.js CHANGED
@@ -90,6 +90,7 @@ var RetryTransport = class {
90
90
  * @param transport The transport to wrap.
91
91
  * @param config Retry configuration.
92
92
  * @throws {RangeError} If retry configuration is invalid.
93
+ * @since 0.5.0
93
94
  */
94
95
  constructor(transport, config = {}) {
95
96
  this.wrappedTransport = transport;
@@ -103,25 +104,16 @@ var RetryTransport = class {
103
104
  * @param options Optional transport options.
104
105
  * @returns The final delivery receipt.
105
106
  * @throws {DOMException} If the operation is aborted.
107
+ * @since 0.5.0
106
108
  */
107
109
  async send(message, options) {
108
110
  options?.signal?.throwIfAborted();
109
111
  let lastThrownError;
110
112
  for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) {
111
113
  options?.signal?.throwIfAborted();
114
+ let receipt;
112
115
  try {
113
- const receipt = await this.wrappedTransport.send(message, options);
114
- if (receipt.successful) return this.withSuccessMetadata(receipt, attempt);
115
- const failedReceipt = this.withFailureMetadata(receipt, attempt);
116
- if (attempt >= this.config.maxAttempts || !this.shouldRetryReceipt(failedReceipt)) return failedReceipt;
117
- await this.waitBeforeRetry({
118
- attempt,
119
- nextAttempt: attempt + 1,
120
- maxAttempts: this.config.maxAttempts,
121
- delayMilliseconds: this.calculateDelay(attempt, failedReceipt),
122
- receipt: failedReceipt,
123
- reason: "retry"
124
- }, options?.signal);
116
+ receipt = await this.wrappedTransport.send(message, options);
125
117
  } catch (error) {
126
118
  options?.signal?.throwIfAborted();
127
119
  lastThrownError = error;
@@ -134,7 +126,20 @@ var RetryTransport = class {
134
126
  error,
135
127
  reason: "retry"
136
128
  }, options?.signal);
129
+ continue;
137
130
  }
131
+ options?.signal?.throwIfAborted();
132
+ if (receipt.successful) return this.withSuccessMetadata(receipt, attempt);
133
+ const failedReceipt = this.withFailureMetadata(receipt, attempt);
134
+ if (attempt >= this.config.maxAttempts || !this.shouldRetryReceipt(failedReceipt)) return failedReceipt;
135
+ await this.waitBeforeRetry({
136
+ attempt,
137
+ nextAttempt: attempt + 1,
138
+ maxAttempts: this.config.maxAttempts,
139
+ delayMilliseconds: this.calculateDelay(attempt, failedReceipt),
140
+ receipt: failedReceipt,
141
+ reason: "retry"
142
+ }, options?.signal);
138
143
  }
139
144
  return this.createThrownFailure(lastThrownError, this.config.maxAttempts);
140
145
  }
@@ -147,6 +152,7 @@ var RetryTransport = class {
147
152
  * @param options Optional transport options.
148
153
  * @returns An async iterable of receipts.
149
154
  * @throws {DOMException} If the operation is aborted.
155
+ * @since 0.5.0
150
156
  */
151
157
  async *sendMany(messages, options) {
152
158
  const iterator = toAsyncIterator(messages);
@@ -170,12 +176,16 @@ var RetryTransport = class {
170
176
  if (inputDone) return;
171
177
  combinedSignal.signal.throwIfAborted();
172
178
  pullingInput = true;
173
- let next;
179
+ let inputPull;
174
180
  try {
175
- next = await iterator.next();
176
- } finally {
177
- pullingInput = false;
181
+ inputPull = Promise.resolve(iterator.next());
182
+ } catch (error) {
183
+ inputPull = Promise.reject(error);
178
184
  }
185
+ inputPull = inputPull.finally(() => {
186
+ pullingInput = false;
187
+ });
188
+ const next = await raceWithAbort(inputPull, combinedSignal.signal);
179
189
  if (closed) return;
180
190
  if (next.done) {
181
191
  inputDone = true;
@@ -196,7 +206,7 @@ var RetryTransport = class {
196
206
  inFlight.set(index, promise);
197
207
  };
198
208
  const startLaunch = () => {
199
- if (launchPromise != null || inputDone || inFlight.size >= this.config.sendMany.maxConcurrent) return;
209
+ if (launchPromise != null || inputDone || hasLaunchError || inFlight.size >= this.config.sendMany.maxConcurrent) return;
200
210
  launchPromise = launchNext().catch((error) => {
201
211
  launchError = error;
202
212
  hasLaunchError = true;
@@ -216,7 +226,7 @@ var RetryTransport = class {
216
226
  nextYieldIndex++;
217
227
  startLaunch();
218
228
  }
219
- if (hasLaunchError) throw launchError;
229
+ if (hasLaunchError && inFlight.size <= 0) throw launchError;
220
230
  startLaunch();
221
231
  if (inFlight.size <= 0 && launchPromise == null) break;
222
232
  const launch = launchPromise?.then(() => ({ launched: true }));
@@ -232,13 +242,10 @@ var RetryTransport = class {
232
242
  controller.abort(new DOMException("The operation was aborted.", "AbortError"));
233
243
  launchPromise?.catch(() => {});
234
244
  try {
235
- const closeIterator = async () => {
236
- if (!inputDone) await iterator.return?.();
237
- };
238
- if (pullingInput) launchPromise?.finally(() => {
239
- closeIterator().catch(() => {});
240
- });
241
- else await closeIterator();
245
+ const closeIterator = () => inputDone ? void 0 : iterator.return?.();
246
+ const closePromise = closeIterator();
247
+ if (pullingInput) closePromise?.catch(() => {});
248
+ else await closePromise;
242
249
  } finally {
243
250
  combinedSignal.cleanup();
244
251
  }
@@ -247,6 +254,8 @@ var RetryTransport = class {
247
254
  /**
248
255
  * Disposes the wrapped transport when it supports disposal.
249
256
  *
257
+ * @throws {unknown} If the wrapped transport's async or sync disposal hook
258
+ * throws or rejects.
250
259
  * @since 0.5.0
251
260
  */
252
261
  async [Symbol.asyncDispose]() {
@@ -301,7 +310,7 @@ var RetryTransport = class {
301
310
  }
302
311
  calculateDelay(attempt, failure) {
303
312
  const retryAfterMilliseconds = getRetryAfterMilliseconds(failure);
304
- const cappedRetryAfter = retryAfterMilliseconds == null ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
313
+ const cappedRetryAfter = retryAfterMilliseconds == null || !Number.isFinite(retryAfterMilliseconds) || retryAfterMilliseconds <= 0 ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
305
314
  if (cappedRetryAfter != null) return cappedRetryAfter;
306
315
  const computedDelay = Math.min(this.config.backoff.baseDelayMilliseconds * Math.pow(this.config.backoff.factor, attempt - 1), this.config.backoff.maxDelayMilliseconds);
307
316
  if (this.config.jitter === false || this.config.jitter === "none") return computedDelay;
@@ -364,8 +373,40 @@ function getRetryAfterMilliseconds(failure) {
364
373
  if (isReceiptError(failure)) return failure.retryAfterMilliseconds;
365
374
  return failure.errors?.find((error) => error.retryAfterMilliseconds != null)?.retryAfterMilliseconds;
366
375
  }
367
- async function* toAsyncIterator(values) {
368
- for await (const value of values) yield value;
376
+ function toAsyncIterator(values) {
377
+ if (Symbol.asyncIterator in values) return values[Symbol.asyncIterator]();
378
+ const iterator = values[Symbol.iterator]();
379
+ return {
380
+ async next() {
381
+ return await Promise.resolve(iterator.next());
382
+ },
383
+ async return(value) {
384
+ return await Promise.resolve(iterator.return?.(value) ?? {
385
+ done: true,
386
+ value
387
+ });
388
+ }
389
+ };
390
+ }
391
+ function raceWithAbort(promise, signal) {
392
+ if (signal.aborted) return Promise.reject(signal.reason);
393
+ return new Promise((resolve, reject) => {
394
+ const abort = () => {
395
+ cleanup();
396
+ reject(signal.reason);
397
+ };
398
+ const cleanup = () => {
399
+ signal.removeEventListener("abort", abort);
400
+ };
401
+ signal.addEventListener("abort", abort, { once: true });
402
+ promise.then((value) => {
403
+ cleanup();
404
+ resolve(value);
405
+ }, (error) => {
406
+ cleanup();
407
+ reject(error);
408
+ });
409
+ });
369
410
  }
370
411
  function toReceiptError(value) {
371
412
  return isReceiptError(value) ? value : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/retry",
3
- "version": "0.5.0-dev.195",
3
+ "version": "0.5.0-dev.199",
4
4
  "description": "Retry and backoff decorator transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "sideEffects": false,
56
56
  "peerDependencies": {
57
- "@upyo/core": "0.5.0-dev.195+5d1ba2de"
57
+ "@upyo/core": "0.5.0-dev.199+f10145fc"
58
58
  },
59
59
  "devDependencies": {
60
60
  "tsdown": "^0.12.7",