@upyo/retry 0.5.0-dev.197 → 0.5.0-dev.201

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);
@@ -181,6 +187,7 @@ var RetryTransport = class {
181
187
  let launchPromise;
182
188
  let launchError;
183
189
  let hasLaunchError = false;
190
+ let hasQueuedFailure = false;
184
191
  let pullingInput = false;
185
192
  let closed = false;
186
193
  const controller = new AbortController();
@@ -193,12 +200,16 @@ var RetryTransport = class {
193
200
  if (inputDone) return;
194
201
  combinedSignal.signal.throwIfAborted();
195
202
  pullingInput = true;
196
- let next;
203
+ let inputPull;
197
204
  try {
198
- next = await iterator.next();
199
- } finally {
200
- pullingInput = false;
205
+ inputPull = Promise.resolve(iterator.next());
206
+ } catch (error) {
207
+ inputPull = Promise.reject(error);
201
208
  }
209
+ inputPull = inputPull.finally(() => {
210
+ pullingInput = false;
211
+ });
212
+ const next = await raceWithAbort(inputPull, combinedSignal.signal);
202
213
  if (closed) return;
203
214
  if (next.done) {
204
215
  inputDone = true;
@@ -219,7 +230,7 @@ var RetryTransport = class {
219
230
  inFlight.set(index, promise);
220
231
  };
221
232
  const startLaunch = () => {
222
- if (launchPromise != null || inputDone || hasLaunchError || inFlight.size >= this.config.sendMany.maxConcurrent) return;
233
+ if (launchPromise != null || inputDone || hasLaunchError || hasQueuedFailure || inFlight.size >= this.config.sendMany.maxConcurrent) return;
223
234
  launchPromise = launchNext().catch((error) => {
224
235
  launchError = error;
225
236
  hasLaunchError = true;
@@ -247,6 +258,7 @@ var RetryTransport = class {
247
258
  if ("launched" in result) continue;
248
259
  inFlight.delete(result.index);
249
260
  completed.set(result.index, result);
261
+ if (!result.successful) hasQueuedFailure = true;
250
262
  startLaunch();
251
263
  }
252
264
  if (hasLaunchError) throw launchError;
@@ -272,15 +284,20 @@ var RetryTransport = class {
272
284
  * @since 0.5.0
273
285
  */
274
286
  async [Symbol.asyncDispose]() {
275
- const asyncDisposable = this.wrappedTransport;
276
- const asyncDispose = asyncDisposable[Symbol.asyncDispose];
277
- if (typeof asyncDispose === "function") {
278
- await asyncDispose.call(asyncDisposable);
279
- return;
287
+ const wrappedTransport = Object(this.wrappedTransport);
288
+ const asyncDisposeSymbol = Symbol.asyncDispose;
289
+ if (typeof asyncDisposeSymbol === "symbol") {
290
+ const asyncDispose = wrappedTransport[asyncDisposeSymbol];
291
+ if (typeof asyncDispose === "function") {
292
+ await asyncDispose.call(wrappedTransport);
293
+ return;
294
+ }
295
+ }
296
+ const disposeSymbol = Symbol.dispose;
297
+ if (typeof disposeSymbol === "symbol") {
298
+ const dispose = wrappedTransport[disposeSymbol];
299
+ if (typeof dispose === "function") dispose.call(wrappedTransport);
280
300
  }
281
- const disposable = this.wrappedTransport;
282
- const dispose = disposable[Symbol.dispose];
283
- if (typeof dispose === "function") dispose.call(disposable);
284
301
  }
285
302
  withSuccessMetadata(receipt, attempts) {
286
303
  return {
@@ -323,7 +340,7 @@ var RetryTransport = class {
323
340
  }
324
341
  calculateDelay(attempt, failure) {
325
342
  const retryAfterMilliseconds = getRetryAfterMilliseconds(failure);
326
- const cappedRetryAfter = retryAfterMilliseconds == null ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
343
+ const cappedRetryAfter = retryAfterMilliseconds == null || !Number.isFinite(retryAfterMilliseconds) || retryAfterMilliseconds <= 0 ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
327
344
  if (cappedRetryAfter != null) return cappedRetryAfter;
328
345
  const computedDelay = Math.min(this.config.backoff.baseDelayMilliseconds * Math.pow(this.config.backoff.factor, attempt - 1), this.config.backoff.maxDelayMilliseconds);
329
346
  if (this.config.jitter === false || this.config.jitter === "none") return computedDelay;
@@ -387,20 +404,44 @@ function getRetryAfterMilliseconds(failure) {
387
404
  return failure.errors?.find((error) => error.retryAfterMilliseconds != null)?.retryAfterMilliseconds;
388
405
  }
389
406
  function toAsyncIterator(values) {
390
- if (Symbol.asyncIterator in values) return values[Symbol.asyncIterator]();
407
+ const asyncIterator = values[Symbol.asyncIterator];
408
+ if (typeof asyncIterator === "function") return asyncIterator.call(values);
391
409
  const iterator = values[Symbol.iterator]();
392
410
  return {
393
- next() {
394
- return Promise.resolve(iterator.next());
411
+ async next() {
412
+ return await Promise.resolve(iterator.next());
395
413
  },
396
- return(value) {
397
- return Promise.resolve(iterator.return?.(value) ?? {
414
+ async return(value) {
415
+ return await Promise.resolve(iterator.return?.(value) ?? {
398
416
  done: true,
399
417
  value
400
418
  });
401
419
  }
402
420
  };
403
421
  }
422
+ function raceWithAbort(promise, signal) {
423
+ if (signal.aborted) return Promise.reject(getAbortReason(signal));
424
+ return new Promise((resolve, reject) => {
425
+ const abort = () => {
426
+ cleanup();
427
+ reject(getAbortReason(signal));
428
+ };
429
+ const cleanup = () => {
430
+ signal.removeEventListener("abort", abort);
431
+ };
432
+ signal.addEventListener("abort", abort, { once: true });
433
+ promise.then((value) => {
434
+ cleanup();
435
+ resolve(value);
436
+ }, (error) => {
437
+ cleanup();
438
+ reject(error);
439
+ });
440
+ });
441
+ }
442
+ function getAbortReason(signal) {
443
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
444
+ }
404
445
  function toReceiptError(value) {
405
446
  return isReceiptError(value) ? value : void 0;
406
447
  }
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,6 +229,7 @@ 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
  /**
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,6 +229,7 @@ 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
  /**
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);
@@ -158,6 +164,7 @@ var RetryTransport = class {
158
164
  let launchPromise;
159
165
  let launchError;
160
166
  let hasLaunchError = false;
167
+ let hasQueuedFailure = false;
161
168
  let pullingInput = false;
162
169
  let closed = false;
163
170
  const controller = new AbortController();
@@ -170,12 +177,16 @@ var RetryTransport = class {
170
177
  if (inputDone) return;
171
178
  combinedSignal.signal.throwIfAborted();
172
179
  pullingInput = true;
173
- let next;
180
+ let inputPull;
174
181
  try {
175
- next = await iterator.next();
176
- } finally {
177
- pullingInput = false;
182
+ inputPull = Promise.resolve(iterator.next());
183
+ } catch (error) {
184
+ inputPull = Promise.reject(error);
178
185
  }
186
+ inputPull = inputPull.finally(() => {
187
+ pullingInput = false;
188
+ });
189
+ const next = await raceWithAbort(inputPull, combinedSignal.signal);
179
190
  if (closed) return;
180
191
  if (next.done) {
181
192
  inputDone = true;
@@ -196,7 +207,7 @@ var RetryTransport = class {
196
207
  inFlight.set(index, promise);
197
208
  };
198
209
  const startLaunch = () => {
199
- if (launchPromise != null || inputDone || hasLaunchError || inFlight.size >= this.config.sendMany.maxConcurrent) return;
210
+ if (launchPromise != null || inputDone || hasLaunchError || hasQueuedFailure || inFlight.size >= this.config.sendMany.maxConcurrent) return;
200
211
  launchPromise = launchNext().catch((error) => {
201
212
  launchError = error;
202
213
  hasLaunchError = true;
@@ -224,6 +235,7 @@ var RetryTransport = class {
224
235
  if ("launched" in result) continue;
225
236
  inFlight.delete(result.index);
226
237
  completed.set(result.index, result);
238
+ if (!result.successful) hasQueuedFailure = true;
227
239
  startLaunch();
228
240
  }
229
241
  if (hasLaunchError) throw launchError;
@@ -249,15 +261,20 @@ var RetryTransport = class {
249
261
  * @since 0.5.0
250
262
  */
251
263
  async [Symbol.asyncDispose]() {
252
- const asyncDisposable = this.wrappedTransport;
253
- const asyncDispose = asyncDisposable[Symbol.asyncDispose];
254
- if (typeof asyncDispose === "function") {
255
- await asyncDispose.call(asyncDisposable);
256
- return;
264
+ const wrappedTransport = Object(this.wrappedTransport);
265
+ const asyncDisposeSymbol = Symbol.asyncDispose;
266
+ if (typeof asyncDisposeSymbol === "symbol") {
267
+ const asyncDispose = wrappedTransport[asyncDisposeSymbol];
268
+ if (typeof asyncDispose === "function") {
269
+ await asyncDispose.call(wrappedTransport);
270
+ return;
271
+ }
272
+ }
273
+ const disposeSymbol = Symbol.dispose;
274
+ if (typeof disposeSymbol === "symbol") {
275
+ const dispose = wrappedTransport[disposeSymbol];
276
+ if (typeof dispose === "function") dispose.call(wrappedTransport);
257
277
  }
258
- const disposable = this.wrappedTransport;
259
- const dispose = disposable[Symbol.dispose];
260
- if (typeof dispose === "function") dispose.call(disposable);
261
278
  }
262
279
  withSuccessMetadata(receipt, attempts) {
263
280
  return {
@@ -300,7 +317,7 @@ var RetryTransport = class {
300
317
  }
301
318
  calculateDelay(attempt, failure) {
302
319
  const retryAfterMilliseconds = getRetryAfterMilliseconds(failure);
303
- const cappedRetryAfter = retryAfterMilliseconds == null ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
320
+ const cappedRetryAfter = retryAfterMilliseconds == null || !Number.isFinite(retryAfterMilliseconds) || retryAfterMilliseconds <= 0 ? void 0 : Math.min(retryAfterMilliseconds, this.config.backoff.maxDelayMilliseconds);
304
321
  if (cappedRetryAfter != null) return cappedRetryAfter;
305
322
  const computedDelay = Math.min(this.config.backoff.baseDelayMilliseconds * Math.pow(this.config.backoff.factor, attempt - 1), this.config.backoff.maxDelayMilliseconds);
306
323
  if (this.config.jitter === false || this.config.jitter === "none") return computedDelay;
@@ -364,20 +381,44 @@ function getRetryAfterMilliseconds(failure) {
364
381
  return failure.errors?.find((error) => error.retryAfterMilliseconds != null)?.retryAfterMilliseconds;
365
382
  }
366
383
  function toAsyncIterator(values) {
367
- if (Symbol.asyncIterator in values) return values[Symbol.asyncIterator]();
384
+ const asyncIterator = values[Symbol.asyncIterator];
385
+ if (typeof asyncIterator === "function") return asyncIterator.call(values);
368
386
  const iterator = values[Symbol.iterator]();
369
387
  return {
370
- next() {
371
- return Promise.resolve(iterator.next());
388
+ async next() {
389
+ return await Promise.resolve(iterator.next());
372
390
  },
373
- return(value) {
374
- return Promise.resolve(iterator.return?.(value) ?? {
391
+ async return(value) {
392
+ return await Promise.resolve(iterator.return?.(value) ?? {
375
393
  done: true,
376
394
  value
377
395
  });
378
396
  }
379
397
  };
380
398
  }
399
+ function raceWithAbort(promise, signal) {
400
+ if (signal.aborted) return Promise.reject(getAbortReason(signal));
401
+ return new Promise((resolve, reject) => {
402
+ const abort = () => {
403
+ cleanup();
404
+ reject(getAbortReason(signal));
405
+ };
406
+ const cleanup = () => {
407
+ signal.removeEventListener("abort", abort);
408
+ };
409
+ signal.addEventListener("abort", abort, { once: true });
410
+ promise.then((value) => {
411
+ cleanup();
412
+ resolve(value);
413
+ }, (error) => {
414
+ cleanup();
415
+ reject(error);
416
+ });
417
+ });
418
+ }
419
+ function getAbortReason(signal) {
420
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
421
+ }
381
422
  function toReceiptError(value) {
382
423
  return isReceiptError(value) ? value : void 0;
383
424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/retry",
3
- "version": "0.5.0-dev.197",
3
+ "version": "0.5.0-dev.201",
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.197+0d26a10d"
57
+ "@upyo/core": "0.5.0-dev.201+ecb7233a"
58
58
  },
59
59
  "devDependencies": {
60
60
  "tsdown": "^0.12.7",