@gravito/signal 3.1.2 → 4.0.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.
@@ -200,6 +200,12 @@ export declare class OrbitSignal implements GravitoOrbit {
200
200
  * ```
201
201
  */
202
202
  install(core: PlanetCore): void;
203
+ /**
204
+ * Gracefully release transport and dev resources.
205
+ *
206
+ * Called during shutdown. Detects closeable transports via type narrowing (no `as any`).
207
+ */
208
+ private cleanup;
203
209
  /**
204
210
  * Internal: Handle processed webhook.
205
211
  */
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Structured error codes for @gravito/signal mail operations.
3
+ * Follows fortify's dot-separated namespace convention.
4
+ *
5
+ * NOTE: This replaces MailErrorCode enum for new code.
6
+ * The existing enum in ../errors.ts remains until Phase 18-19 migration.
7
+ *
8
+ * @public
9
+ */
10
+ export declare const MailErrorCodes: {
11
+ readonly CONNECTION_FAILED: "mail.connection_failed";
12
+ readonly AUTH_FAILED: "mail.auth_failed";
13
+ readonly RECIPIENT_REJECTED: "mail.recipient_rejected";
14
+ readonly MESSAGE_REJECTED: "mail.message_rejected";
15
+ readonly RATE_LIMIT: "mail.rate_limit";
16
+ readonly SEND_FAILED: "mail.send_failed";
17
+ readonly UNKNOWN: "mail.unknown";
18
+ };
19
+ export type MailErrorCode = (typeof MailErrorCodes)[keyof typeof MailErrorCodes];
package/dist/errors.d.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import { InfrastructureException } from '@gravito/core';
2
+ import { type MailErrorCode as MailErrorCodeType } from './errors/codes';
1
3
  /**
2
- * Mail transport error codes.
3
- *
4
- * Categorizes common failure modes in the mail delivery process to allow
5
- * for programmatic handling (e.g., retries on rate limits).
4
+ * Mail transport error codes (legacy enum — kept for backward compat).
5
+ * New code should use MailErrorCodes from './errors/codes' instead.
6
6
  *
7
+ * @deprecated Use MailErrorCodes from './errors/codes'
7
8
  * @public
8
9
  * @since 3.1.0
9
10
  */
@@ -24,8 +25,9 @@ export declare enum MailErrorCode {
24
25
  /**
25
26
  * Error class for mail transport failures.
26
27
  *
27
- * Provides structured error information for mail sending failures,
28
- * including error codes and original cause tracking for debugging.
28
+ * Extends InfrastructureException for unified error handling across Gravito.
29
+ * Carries a `retryable` flag indicating whether the operation can be retried,
30
+ * and preserves backward compat with the legacy MailErrorCode enum via `.legacyCode`.
29
31
  *
30
32
  * @example
31
33
  * ```typescript
@@ -39,14 +41,19 @@ export declare enum MailErrorCode {
39
41
  * @public
40
42
  * @since 3.1.0
41
43
  */
42
- export declare class MailTransportError extends Error {
43
- readonly code: MailErrorCode;
44
- readonly cause?: Error;
44
+ export declare class MailTransportError extends InfrastructureException {
45
+ /**
46
+ * Legacy enum code — preserved for backward compat with callers that switch on MailErrorCode.
47
+ * @deprecated Prefer checking `.code` (mail.* namespaced string) instead.
48
+ */
49
+ readonly legacyCode: MailErrorCode;
45
50
  /**
46
51
  * Create a new mail transport error.
47
52
  *
53
+ * Accepts both the legacy MailErrorCode enum and new mail.* namespaced string codes.
54
+ *
48
55
  * @param message - Human-readable error message
49
- * @param code - Categorized error code
56
+ * @param code - Categorized error code (legacy enum or new mail.* string)
50
57
  * @param cause - Original error that caused this failure
51
58
  *
52
59
  * @example
@@ -54,5 +61,5 @@ export declare class MailTransportError extends Error {
54
61
  * const error = new MailTransportError('Auth failed', MailErrorCode.AUTH_FAILED);
55
62
  * ```
56
63
  */
57
- constructor(message: string, code: MailErrorCode, cause?: Error);
64
+ constructor(message: string, code?: MailErrorCode | MailErrorCodeType, cause?: Error);
58
65
  }
package/dist/index.cjs CHANGED
@@ -59693,6 +59693,7 @@ __export(index_exports, {
59693
59693
  HtmlRenderer: () => HtmlRenderer,
59694
59694
  LogTransport: () => LogTransport,
59695
59695
  MailErrorCode: () => MailErrorCode,
59696
+ MailErrorCodes: () => MailErrorCodes,
59696
59697
  MailTransportError: () => MailTransportError,
59697
59698
  Mailable: () => Mailable,
59698
59699
  MemoryTransport: () => MemoryTransport,
@@ -59838,6 +59839,20 @@ var DevMailbox = class {
59838
59839
  }
59839
59840
  };
59840
59841
 
59842
+ // src/errors.ts
59843
+ var import_core = require("@gravito/core");
59844
+
59845
+ // src/errors/codes.ts
59846
+ var MailErrorCodes = {
59847
+ CONNECTION_FAILED: "mail.connection_failed",
59848
+ AUTH_FAILED: "mail.auth_failed",
59849
+ RECIPIENT_REJECTED: "mail.recipient_rejected",
59850
+ MESSAGE_REJECTED: "mail.message_rejected",
59851
+ RATE_LIMIT: "mail.rate_limit",
59852
+ SEND_FAILED: "mail.send_failed",
59853
+ UNKNOWN: "mail.unknown"
59854
+ };
59855
+
59841
59856
  // src/errors.ts
59842
59857
  var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
59843
59858
  MailErrorCode2["CONNECTION_FAILED"] = "CONNECTION_FAILED";
@@ -59848,12 +59863,31 @@ var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
59848
59863
  MailErrorCode2["UNKNOWN"] = "UNKNOWN";
59849
59864
  return MailErrorCode2;
59850
59865
  })(MailErrorCode || {});
59851
- var MailTransportError = class _MailTransportError extends Error {
59866
+ var legacyToNewCode = {
59867
+ ["CONNECTION_FAILED" /* CONNECTION_FAILED */]: MailErrorCodes.CONNECTION_FAILED,
59868
+ ["AUTH_FAILED" /* AUTH_FAILED */]: MailErrorCodes.AUTH_FAILED,
59869
+ ["RECIPIENT_REJECTED" /* RECIPIENT_REJECTED */]: MailErrorCodes.RECIPIENT_REJECTED,
59870
+ ["MESSAGE_REJECTED" /* MESSAGE_REJECTED */]: MailErrorCodes.MESSAGE_REJECTED,
59871
+ ["RATE_LIMIT" /* RATE_LIMIT */]: MailErrorCodes.RATE_LIMIT,
59872
+ ["UNKNOWN" /* UNKNOWN */]: MailErrorCodes.UNKNOWN
59873
+ };
59874
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
59875
+ MailErrorCodes.CONNECTION_FAILED,
59876
+ MailErrorCodes.RATE_LIMIT
59877
+ ]);
59878
+ var MailTransportError = class _MailTransportError extends import_core.InfrastructureException {
59879
+ /**
59880
+ * Legacy enum code — preserved for backward compat with callers that switch on MailErrorCode.
59881
+ * @deprecated Prefer checking `.code` (mail.* namespaced string) instead.
59882
+ */
59883
+ legacyCode;
59852
59884
  /**
59853
59885
  * Create a new mail transport error.
59854
59886
  *
59887
+ * Accepts both the legacy MailErrorCode enum and new mail.* namespaced string codes.
59888
+ *
59855
59889
  * @param message - Human-readable error message
59856
- * @param code - Categorized error code
59890
+ * @param code - Categorized error code (legacy enum or new mail.* string)
59857
59891
  * @param cause - Original error that caused this failure
59858
59892
  *
59859
59893
  * @example
@@ -59861,11 +59895,16 @@ var MailTransportError = class _MailTransportError extends Error {
59861
59895
  * const error = new MailTransportError('Auth failed', MailErrorCode.AUTH_FAILED);
59862
59896
  * ```
59863
59897
  */
59864
- constructor(message, code, cause) {
59865
- super(message);
59866
- this.code = code;
59867
- this.cause = cause;
59898
+ constructor(message, code = "UNKNOWN" /* UNKNOWN */, cause) {
59899
+ const newCode = typeof code === "string" && code.startsWith("mail.") ? code : legacyToNewCode[code] ?? MailErrorCodes.UNKNOWN;
59900
+ super(502, newCode, {
59901
+ message,
59902
+ cause,
59903
+ retryable: RETRYABLE_CODES.has(newCode)
59904
+ });
59868
59905
  this.name = "MailTransportError";
59906
+ this.legacyCode = typeof code === "string" && code.startsWith("mail.") ? Object.entries(legacyToNewCode).find(([, v]) => v === code)?.[0] ?? "UNKNOWN" /* UNKNOWN */ : code;
59907
+ Object.setPrototypeOf(this, new.target.prototype);
59869
59908
  if (Error.captureStackTrace) {
59870
59909
  Error.captureStackTrace(this, _MailTransportError);
59871
59910
  }
@@ -60851,6 +60890,9 @@ var MemoryTransport = class {
60851
60890
  };
60852
60891
 
60853
60892
  // src/OrbitSignal.ts
60893
+ function isCloseable(obj) {
60894
+ return typeof obj === "object" && obj !== null && "close" in obj && typeof obj.close === "function";
60895
+ }
60854
60896
  var OrbitSignal = class {
60855
60897
  config;
60856
60898
  devMailbox;
@@ -60916,6 +60958,34 @@ var OrbitSignal = class {
60916
60958
  }
60917
60959
  });
60918
60960
  }
60961
+ core.hooks.doAction("core:shutdown", async () => {
60962
+ const DEADLINE_MS = 5e3;
60963
+ const deadline = new Promise(
60964
+ (_, reject) => setTimeout(
60965
+ () => reject(new Error("[OrbitSignal] Shutdown deadline exceeded (5s)")),
60966
+ DEADLINE_MS
60967
+ )
60968
+ );
60969
+ try {
60970
+ await Promise.race([this.cleanup(), deadline]);
60971
+ } catch (err) {
60972
+ core.logger.warn("[OrbitSignal] Forced shutdown:", err);
60973
+ }
60974
+ });
60975
+ }
60976
+ /**
60977
+ * Gracefully release transport and dev resources.
60978
+ *
60979
+ * Called during shutdown. Detects closeable transports via type narrowing (no `as any`).
60980
+ */
60981
+ async cleanup() {
60982
+ if (this.devMailbox) {
60983
+ this.devMailbox = void 0;
60984
+ }
60985
+ const transport = this.config.transport;
60986
+ if (isCloseable(transport)) {
60987
+ await transport.close();
60988
+ }
60919
60989
  }
60920
60990
  /**
60921
60991
  * Internal: Handle processed webhook.
@@ -61111,6 +61181,7 @@ init_ReactMjmlRenderer();
61111
61181
  init_VueMjmlRenderer();
61112
61182
 
61113
61183
  // src/transports/BaseTransport.ts
61184
+ var import_resilience = require("@gravito/resilience");
61114
61185
  var BaseTransport = class {
61115
61186
  options;
61116
61187
  /**
@@ -61126,14 +61197,14 @@ var BaseTransport = class {
61126
61197
  };
61127
61198
  }
61128
61199
  /**
61129
- * Orchestrates the message delivery with retry logic.
61200
+ * Orchestrates the message delivery with automatic retry via @gravito/resilience.
61130
61201
  *
61131
- * This method wraps the concrete `doSend` implementation in a retry loop.
61132
- * It tracks the last error encountered to provide context if all retries fail.
61202
+ * Delegates to `doSend()` and retries on retryable InfrastructureException errors
61203
+ * (CONNECTION_FAILED, RATE_LIMIT). Non-retryable errors surface immediately.
61133
61204
  *
61134
61205
  * @param message - The message to be delivered.
61135
61206
  * @returns A promise that resolves when the message is successfully sent.
61136
- * @throws {MailTransportError} If the message cannot be sent after the maximum number of retries.
61207
+ * @throws {MailTransportError} If the message cannot be sent after the maximum number of attempts.
61137
61208
  *
61138
61209
  * @example
61139
61210
  * ```typescript
@@ -61146,33 +61217,22 @@ var BaseTransport = class {
61146
61217
  * ```
61147
61218
  */
61148
61219
  async send(message) {
61149
- let lastError;
61150
- let delay = this.options.retryDelay;
61151
- for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
61152
- try {
61153
- return await this.doSend(message);
61154
- } catch (error) {
61155
- lastError = error;
61156
- if (attempt < this.options.maxRetries) {
61157
- await this.sleep(delay);
61158
- delay *= this.options.backoffMultiplier;
61159
- }
61220
+ try {
61221
+ await (0, import_resilience.withRetry)(() => this.doSend(message), {
61222
+ idempotent: true,
61223
+ maxAttempts: this.options.maxRetries,
61224
+ baseDelayMs: this.options.retryDelay
61225
+ });
61226
+ } catch (error) {
61227
+ if (error instanceof MailTransportError) {
61228
+ throw error;
61160
61229
  }
61230
+ throw new MailTransportError(
61231
+ `Mail sending failed after ${this.options.maxRetries} retries`,
61232
+ "UNKNOWN" /* UNKNOWN */,
61233
+ error
61234
+ );
61161
61235
  }
61162
- throw new MailTransportError(
61163
- `Mail sending failed after ${this.options.maxRetries} retries`,
61164
- "UNKNOWN" /* UNKNOWN */,
61165
- lastError
61166
- );
61167
- }
61168
- /**
61169
- * Utility method to pause execution for a given duration.
61170
- *
61171
- * @param ms - Milliseconds to sleep.
61172
- * @returns A promise that resolves after the delay.
61173
- */
61174
- sleep(ms) {
61175
- return new Promise((resolve) => setTimeout(resolve, ms));
61176
61236
  }
61177
61237
  };
61178
61238
 
@@ -61452,6 +61512,7 @@ var SesWebhookDriver = class {
61452
61512
  HtmlRenderer,
61453
61513
  LogTransport,
61454
61514
  MailErrorCode,
61515
+ MailErrorCodes,
61455
61516
  MailTransportError,
61456
61517
  Mailable,
61457
61518
  MemoryTransport,
package/dist/index.d.ts CHANGED
@@ -54,6 +54,7 @@ export { DevMailbox, type MailboxEntry } from './dev/DevMailbox';
54
54
  * @public
55
55
  */
56
56
  export { MailErrorCode, MailTransportError } from './errors';
57
+ export { MailErrorCodes } from './errors/codes';
57
58
  /**
58
59
  * Mail lifecycle event types and handlers.
59
60
  *
package/dist/index.js CHANGED
@@ -59693,6 +59693,7 @@ __export(index_exports, {
59693
59693
  HtmlRenderer: () => HtmlRenderer,
59694
59694
  LogTransport: () => LogTransport,
59695
59695
  MailErrorCode: () => MailErrorCode,
59696
+ MailErrorCodes: () => MailErrorCodes,
59696
59697
  MailTransportError: () => MailTransportError,
59697
59698
  Mailable: () => Mailable,
59698
59699
  MemoryTransport: () => MemoryTransport,
@@ -59838,6 +59839,20 @@ var DevMailbox = class {
59838
59839
  }
59839
59840
  };
59840
59841
 
59842
+ // src/errors.ts
59843
+ var import_core = require("@gravito/core");
59844
+
59845
+ // src/errors/codes.ts
59846
+ var MailErrorCodes = {
59847
+ CONNECTION_FAILED: "mail.connection_failed",
59848
+ AUTH_FAILED: "mail.auth_failed",
59849
+ RECIPIENT_REJECTED: "mail.recipient_rejected",
59850
+ MESSAGE_REJECTED: "mail.message_rejected",
59851
+ RATE_LIMIT: "mail.rate_limit",
59852
+ SEND_FAILED: "mail.send_failed",
59853
+ UNKNOWN: "mail.unknown"
59854
+ };
59855
+
59841
59856
  // src/errors.ts
59842
59857
  var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
59843
59858
  MailErrorCode2["CONNECTION_FAILED"] = "CONNECTION_FAILED";
@@ -59848,12 +59863,31 @@ var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
59848
59863
  MailErrorCode2["UNKNOWN"] = "UNKNOWN";
59849
59864
  return MailErrorCode2;
59850
59865
  })(MailErrorCode || {});
59851
- var MailTransportError = class _MailTransportError extends Error {
59866
+ var legacyToNewCode = {
59867
+ ["CONNECTION_FAILED" /* CONNECTION_FAILED */]: MailErrorCodes.CONNECTION_FAILED,
59868
+ ["AUTH_FAILED" /* AUTH_FAILED */]: MailErrorCodes.AUTH_FAILED,
59869
+ ["RECIPIENT_REJECTED" /* RECIPIENT_REJECTED */]: MailErrorCodes.RECIPIENT_REJECTED,
59870
+ ["MESSAGE_REJECTED" /* MESSAGE_REJECTED */]: MailErrorCodes.MESSAGE_REJECTED,
59871
+ ["RATE_LIMIT" /* RATE_LIMIT */]: MailErrorCodes.RATE_LIMIT,
59872
+ ["UNKNOWN" /* UNKNOWN */]: MailErrorCodes.UNKNOWN
59873
+ };
59874
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
59875
+ MailErrorCodes.CONNECTION_FAILED,
59876
+ MailErrorCodes.RATE_LIMIT
59877
+ ]);
59878
+ var MailTransportError = class _MailTransportError extends import_core.InfrastructureException {
59879
+ /**
59880
+ * Legacy enum code — preserved for backward compat with callers that switch on MailErrorCode.
59881
+ * @deprecated Prefer checking `.code` (mail.* namespaced string) instead.
59882
+ */
59883
+ legacyCode;
59852
59884
  /**
59853
59885
  * Create a new mail transport error.
59854
59886
  *
59887
+ * Accepts both the legacy MailErrorCode enum and new mail.* namespaced string codes.
59888
+ *
59855
59889
  * @param message - Human-readable error message
59856
- * @param code - Categorized error code
59890
+ * @param code - Categorized error code (legacy enum or new mail.* string)
59857
59891
  * @param cause - Original error that caused this failure
59858
59892
  *
59859
59893
  * @example
@@ -59861,11 +59895,16 @@ var MailTransportError = class _MailTransportError extends Error {
59861
59895
  * const error = new MailTransportError('Auth failed', MailErrorCode.AUTH_FAILED);
59862
59896
  * ```
59863
59897
  */
59864
- constructor(message, code, cause) {
59865
- super(message);
59866
- this.code = code;
59867
- this.cause = cause;
59898
+ constructor(message, code = "UNKNOWN" /* UNKNOWN */, cause) {
59899
+ const newCode = typeof code === "string" && code.startsWith("mail.") ? code : legacyToNewCode[code] ?? MailErrorCodes.UNKNOWN;
59900
+ super(502, newCode, {
59901
+ message,
59902
+ cause,
59903
+ retryable: RETRYABLE_CODES.has(newCode)
59904
+ });
59868
59905
  this.name = "MailTransportError";
59906
+ this.legacyCode = typeof code === "string" && code.startsWith("mail.") ? Object.entries(legacyToNewCode).find(([, v]) => v === code)?.[0] ?? "UNKNOWN" /* UNKNOWN */ : code;
59907
+ Object.setPrototypeOf(this, new.target.prototype);
59869
59908
  if (Error.captureStackTrace) {
59870
59909
  Error.captureStackTrace(this, _MailTransportError);
59871
59910
  }
@@ -60851,6 +60890,9 @@ var MemoryTransport = class {
60851
60890
  };
60852
60891
 
60853
60892
  // src/OrbitSignal.ts
60893
+ function isCloseable(obj) {
60894
+ return typeof obj === "object" && obj !== null && "close" in obj && typeof obj.close === "function";
60895
+ }
60854
60896
  var OrbitSignal = class {
60855
60897
  config;
60856
60898
  devMailbox;
@@ -60916,6 +60958,34 @@ var OrbitSignal = class {
60916
60958
  }
60917
60959
  });
60918
60960
  }
60961
+ core.hooks.doAction("core:shutdown", async () => {
60962
+ const DEADLINE_MS = 5e3;
60963
+ const deadline = new Promise(
60964
+ (_, reject) => setTimeout(
60965
+ () => reject(new Error("[OrbitSignal] Shutdown deadline exceeded (5s)")),
60966
+ DEADLINE_MS
60967
+ )
60968
+ );
60969
+ try {
60970
+ await Promise.race([this.cleanup(), deadline]);
60971
+ } catch (err) {
60972
+ core.logger.warn("[OrbitSignal] Forced shutdown:", err);
60973
+ }
60974
+ });
60975
+ }
60976
+ /**
60977
+ * Gracefully release transport and dev resources.
60978
+ *
60979
+ * Called during shutdown. Detects closeable transports via type narrowing (no `as any`).
60980
+ */
60981
+ async cleanup() {
60982
+ if (this.devMailbox) {
60983
+ this.devMailbox = void 0;
60984
+ }
60985
+ const transport = this.config.transport;
60986
+ if (isCloseable(transport)) {
60987
+ await transport.close();
60988
+ }
60919
60989
  }
60920
60990
  /**
60921
60991
  * Internal: Handle processed webhook.
@@ -61111,6 +61181,7 @@ init_ReactMjmlRenderer();
61111
61181
  init_VueMjmlRenderer();
61112
61182
 
61113
61183
  // src/transports/BaseTransport.ts
61184
+ var import_resilience = require("@gravito/resilience");
61114
61185
  var BaseTransport = class {
61115
61186
  options;
61116
61187
  /**
@@ -61126,14 +61197,14 @@ var BaseTransport = class {
61126
61197
  };
61127
61198
  }
61128
61199
  /**
61129
- * Orchestrates the message delivery with retry logic.
61200
+ * Orchestrates the message delivery with automatic retry via @gravito/resilience.
61130
61201
  *
61131
- * This method wraps the concrete `doSend` implementation in a retry loop.
61132
- * It tracks the last error encountered to provide context if all retries fail.
61202
+ * Delegates to `doSend()` and retries on retryable InfrastructureException errors
61203
+ * (CONNECTION_FAILED, RATE_LIMIT). Non-retryable errors surface immediately.
61133
61204
  *
61134
61205
  * @param message - The message to be delivered.
61135
61206
  * @returns A promise that resolves when the message is successfully sent.
61136
- * @throws {MailTransportError} If the message cannot be sent after the maximum number of retries.
61207
+ * @throws {MailTransportError} If the message cannot be sent after the maximum number of attempts.
61137
61208
  *
61138
61209
  * @example
61139
61210
  * ```typescript
@@ -61146,33 +61217,22 @@ var BaseTransport = class {
61146
61217
  * ```
61147
61218
  */
61148
61219
  async send(message) {
61149
- let lastError;
61150
- let delay = this.options.retryDelay;
61151
- for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
61152
- try {
61153
- return await this.doSend(message);
61154
- } catch (error) {
61155
- lastError = error;
61156
- if (attempt < this.options.maxRetries) {
61157
- await this.sleep(delay);
61158
- delay *= this.options.backoffMultiplier;
61159
- }
61220
+ try {
61221
+ await (0, import_resilience.withRetry)(() => this.doSend(message), {
61222
+ idempotent: true,
61223
+ maxAttempts: this.options.maxRetries,
61224
+ baseDelayMs: this.options.retryDelay
61225
+ });
61226
+ } catch (error) {
61227
+ if (error instanceof MailTransportError) {
61228
+ throw error;
61160
61229
  }
61230
+ throw new MailTransportError(
61231
+ `Mail sending failed after ${this.options.maxRetries} retries`,
61232
+ "UNKNOWN" /* UNKNOWN */,
61233
+ error
61234
+ );
61161
61235
  }
61162
- throw new MailTransportError(
61163
- `Mail sending failed after ${this.options.maxRetries} retries`,
61164
- "UNKNOWN" /* UNKNOWN */,
61165
- lastError
61166
- );
61167
- }
61168
- /**
61169
- * Utility method to pause execution for a given duration.
61170
- *
61171
- * @param ms - Milliseconds to sleep.
61172
- * @returns A promise that resolves after the delay.
61173
- */
61174
- sleep(ms) {
61175
- return new Promise((resolve) => setTimeout(resolve, ms));
61176
61236
  }
61177
61237
  };
61178
61238
 
@@ -61452,6 +61512,7 @@ var SesWebhookDriver = class {
61452
61512
  HtmlRenderer,
61453
61513
  LogTransport,
61454
61514
  MailErrorCode,
61515
+ MailErrorCodes,
61455
61516
  MailTransportError,
61456
61517
  Mailable,
61457
61518
  MemoryTransport,
package/dist/index.mjs CHANGED
@@ -59817,6 +59817,20 @@ var DevMailbox = class {
59817
59817
  }
59818
59818
  };
59819
59819
 
59820
+ // src/errors.ts
59821
+ import { InfrastructureException } from "@gravito/core";
59822
+
59823
+ // src/errors/codes.ts
59824
+ var MailErrorCodes = {
59825
+ CONNECTION_FAILED: "mail.connection_failed",
59826
+ AUTH_FAILED: "mail.auth_failed",
59827
+ RECIPIENT_REJECTED: "mail.recipient_rejected",
59828
+ MESSAGE_REJECTED: "mail.message_rejected",
59829
+ RATE_LIMIT: "mail.rate_limit",
59830
+ SEND_FAILED: "mail.send_failed",
59831
+ UNKNOWN: "mail.unknown"
59832
+ };
59833
+
59820
59834
  // src/errors.ts
59821
59835
  var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
59822
59836
  MailErrorCode2["CONNECTION_FAILED"] = "CONNECTION_FAILED";
@@ -59827,12 +59841,31 @@ var MailErrorCode = /* @__PURE__ */ ((MailErrorCode2) => {
59827
59841
  MailErrorCode2["UNKNOWN"] = "UNKNOWN";
59828
59842
  return MailErrorCode2;
59829
59843
  })(MailErrorCode || {});
59830
- var MailTransportError = class _MailTransportError extends Error {
59844
+ var legacyToNewCode = {
59845
+ ["CONNECTION_FAILED" /* CONNECTION_FAILED */]: MailErrorCodes.CONNECTION_FAILED,
59846
+ ["AUTH_FAILED" /* AUTH_FAILED */]: MailErrorCodes.AUTH_FAILED,
59847
+ ["RECIPIENT_REJECTED" /* RECIPIENT_REJECTED */]: MailErrorCodes.RECIPIENT_REJECTED,
59848
+ ["MESSAGE_REJECTED" /* MESSAGE_REJECTED */]: MailErrorCodes.MESSAGE_REJECTED,
59849
+ ["RATE_LIMIT" /* RATE_LIMIT */]: MailErrorCodes.RATE_LIMIT,
59850
+ ["UNKNOWN" /* UNKNOWN */]: MailErrorCodes.UNKNOWN
59851
+ };
59852
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
59853
+ MailErrorCodes.CONNECTION_FAILED,
59854
+ MailErrorCodes.RATE_LIMIT
59855
+ ]);
59856
+ var MailTransportError = class _MailTransportError extends InfrastructureException {
59857
+ /**
59858
+ * Legacy enum code — preserved for backward compat with callers that switch on MailErrorCode.
59859
+ * @deprecated Prefer checking `.code` (mail.* namespaced string) instead.
59860
+ */
59861
+ legacyCode;
59831
59862
  /**
59832
59863
  * Create a new mail transport error.
59833
59864
  *
59865
+ * Accepts both the legacy MailErrorCode enum and new mail.* namespaced string codes.
59866
+ *
59834
59867
  * @param message - Human-readable error message
59835
- * @param code - Categorized error code
59868
+ * @param code - Categorized error code (legacy enum or new mail.* string)
59836
59869
  * @param cause - Original error that caused this failure
59837
59870
  *
59838
59871
  * @example
@@ -59840,11 +59873,16 @@ var MailTransportError = class _MailTransportError extends Error {
59840
59873
  * const error = new MailTransportError('Auth failed', MailErrorCode.AUTH_FAILED);
59841
59874
  * ```
59842
59875
  */
59843
- constructor(message, code, cause) {
59844
- super(message);
59845
- this.code = code;
59846
- this.cause = cause;
59876
+ constructor(message, code = "UNKNOWN" /* UNKNOWN */, cause) {
59877
+ const newCode = typeof code === "string" && code.startsWith("mail.") ? code : legacyToNewCode[code] ?? MailErrorCodes.UNKNOWN;
59878
+ super(502, newCode, {
59879
+ message,
59880
+ cause,
59881
+ retryable: RETRYABLE_CODES.has(newCode)
59882
+ });
59847
59883
  this.name = "MailTransportError";
59884
+ this.legacyCode = typeof code === "string" && code.startsWith("mail.") ? Object.entries(legacyToNewCode).find(([, v]) => v === code)?.[0] ?? "UNKNOWN" /* UNKNOWN */ : code;
59885
+ Object.setPrototypeOf(this, new.target.prototype);
59848
59886
  if (Error.captureStackTrace) {
59849
59887
  Error.captureStackTrace(this, _MailTransportError);
59850
59888
  }
@@ -60830,6 +60868,9 @@ var MemoryTransport = class {
60830
60868
  };
60831
60869
 
60832
60870
  // src/OrbitSignal.ts
60871
+ function isCloseable(obj) {
60872
+ return typeof obj === "object" && obj !== null && "close" in obj && typeof obj.close === "function";
60873
+ }
60833
60874
  var OrbitSignal = class {
60834
60875
  config;
60835
60876
  devMailbox;
@@ -60895,6 +60936,34 @@ var OrbitSignal = class {
60895
60936
  }
60896
60937
  });
60897
60938
  }
60939
+ core.hooks.doAction("core:shutdown", async () => {
60940
+ const DEADLINE_MS = 5e3;
60941
+ const deadline = new Promise(
60942
+ (_, reject) => setTimeout(
60943
+ () => reject(new Error("[OrbitSignal] Shutdown deadline exceeded (5s)")),
60944
+ DEADLINE_MS
60945
+ )
60946
+ );
60947
+ try {
60948
+ await Promise.race([this.cleanup(), deadline]);
60949
+ } catch (err) {
60950
+ core.logger.warn("[OrbitSignal] Forced shutdown:", err);
60951
+ }
60952
+ });
60953
+ }
60954
+ /**
60955
+ * Gracefully release transport and dev resources.
60956
+ *
60957
+ * Called during shutdown. Detects closeable transports via type narrowing (no `as any`).
60958
+ */
60959
+ async cleanup() {
60960
+ if (this.devMailbox) {
60961
+ this.devMailbox = void 0;
60962
+ }
60963
+ const transport = this.config.transport;
60964
+ if (isCloseable(transport)) {
60965
+ await transport.close();
60966
+ }
60898
60967
  }
60899
60968
  /**
60900
60969
  * Internal: Handle processed webhook.
@@ -61090,6 +61159,7 @@ init_ReactMjmlRenderer();
61090
61159
  init_VueMjmlRenderer();
61091
61160
 
61092
61161
  // src/transports/BaseTransport.ts
61162
+ import { withRetry } from "@gravito/resilience";
61093
61163
  var BaseTransport = class {
61094
61164
  options;
61095
61165
  /**
@@ -61105,14 +61175,14 @@ var BaseTransport = class {
61105
61175
  };
61106
61176
  }
61107
61177
  /**
61108
- * Orchestrates the message delivery with retry logic.
61178
+ * Orchestrates the message delivery with automatic retry via @gravito/resilience.
61109
61179
  *
61110
- * This method wraps the concrete `doSend` implementation in a retry loop.
61111
- * It tracks the last error encountered to provide context if all retries fail.
61180
+ * Delegates to `doSend()` and retries on retryable InfrastructureException errors
61181
+ * (CONNECTION_FAILED, RATE_LIMIT). Non-retryable errors surface immediately.
61112
61182
  *
61113
61183
  * @param message - The message to be delivered.
61114
61184
  * @returns A promise that resolves when the message is successfully sent.
61115
- * @throws {MailTransportError} If the message cannot be sent after the maximum number of retries.
61185
+ * @throws {MailTransportError} If the message cannot be sent after the maximum number of attempts.
61116
61186
  *
61117
61187
  * @example
61118
61188
  * ```typescript
@@ -61125,33 +61195,22 @@ var BaseTransport = class {
61125
61195
  * ```
61126
61196
  */
61127
61197
  async send(message) {
61128
- let lastError;
61129
- let delay = this.options.retryDelay;
61130
- for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
61131
- try {
61132
- return await this.doSend(message);
61133
- } catch (error) {
61134
- lastError = error;
61135
- if (attempt < this.options.maxRetries) {
61136
- await this.sleep(delay);
61137
- delay *= this.options.backoffMultiplier;
61138
- }
61198
+ try {
61199
+ await withRetry(() => this.doSend(message), {
61200
+ idempotent: true,
61201
+ maxAttempts: this.options.maxRetries,
61202
+ baseDelayMs: this.options.retryDelay
61203
+ });
61204
+ } catch (error) {
61205
+ if (error instanceof MailTransportError) {
61206
+ throw error;
61139
61207
  }
61208
+ throw new MailTransportError(
61209
+ `Mail sending failed after ${this.options.maxRetries} retries`,
61210
+ "UNKNOWN" /* UNKNOWN */,
61211
+ error
61212
+ );
61140
61213
  }
61141
- throw new MailTransportError(
61142
- `Mail sending failed after ${this.options.maxRetries} retries`,
61143
- "UNKNOWN" /* UNKNOWN */,
61144
- lastError
61145
- );
61146
- }
61147
- /**
61148
- * Utility method to pause execution for a given duration.
61149
- *
61150
- * @param ms - Milliseconds to sleep.
61151
- * @returns A promise that resolves after the delay.
61152
- */
61153
- sleep(ms) {
61154
- return new Promise((resolve) => setTimeout(resolve, ms));
61155
61214
  }
61156
61215
  };
61157
61216
 
@@ -61430,6 +61489,7 @@ export {
61430
61489
  HtmlRenderer,
61431
61490
  LogTransport,
61432
61491
  MailErrorCode,
61492
+ MailErrorCodes,
61433
61493
  MailTransportError,
61434
61494
  Mailable,
61435
61495
  MemoryTransport,
@@ -5,6 +5,9 @@ import type { Message, Transport } from '../types';
5
5
  * Defines the behavior of the automatic retry mechanism, including the number of attempts
6
6
  * and the timing between them using exponential backoff.
7
7
  *
8
+ * @deprecated Configuration now handled internally by @gravito/resilience RetryOptions.
9
+ * Kept for backward compat of existing subclass constructors.
10
+ *
8
11
  * @example
9
12
  * ```typescript
10
13
  * const options: TransportOptions = {
@@ -29,21 +32,17 @@ export interface TransportOptions {
29
32
  /**
30
33
  * Multiplier applied to the delay after each failed attempt.
31
34
  * Used to implement exponential backoff to avoid overwhelming the service.
35
+ * @deprecated Backoff is now managed by @gravito/resilience (ExponentialBackoff).
32
36
  */
33
37
  backoffMultiplier?: number;
34
38
  }
35
39
  /**
36
- * Base transport class with automatic retry mechanism.
40
+ * Base transport class with automatic retry via @gravito/resilience.
37
41
  *
38
42
  * This abstract class provides a robust foundation for all transport implementations by
39
- * handling transient failures through an exponential backoff retry strategy. It ensures
40
- * that temporary network issues or service rate limits do not immediately fail the email delivery.
41
- *
42
- * The retry mechanism works as follows:
43
- * 1. Attempt to send the message via `doSend()`.
44
- * 2. If it fails, wait for `retryDelay` milliseconds.
45
- * 3. Increment the delay by `backoffMultiplier` for the next attempt.
46
- * 4. Repeat until success or `maxRetries` is reached.
43
+ * handling transient failures through the unified @gravito/resilience withRetry primitive.
44
+ * Only errors whose `retryable` flag is true (e.g., CONNECTION_FAILED, RATE_LIMIT) are
45
+ * retried automatically.
47
46
  *
48
47
  * @example
49
48
  * ```typescript
@@ -54,7 +53,7 @@ export interface TransportOptions {
54
53
  *
55
54
  * protected async doSend(message: Message): Promise<void> {
56
55
  * // Actual implementation of the sending logic
57
- * // If this throws, BaseTransport will catch and retry
56
+ * // If this throws, BaseTransport will retry via withRetry
58
57
  * }
59
58
  * }
60
59
  * ```
@@ -70,14 +69,14 @@ export declare abstract class BaseTransport implements Transport {
70
69
  */
71
70
  constructor(options?: TransportOptions);
72
71
  /**
73
- * Orchestrates the message delivery with retry logic.
72
+ * Orchestrates the message delivery with automatic retry via @gravito/resilience.
74
73
  *
75
- * This method wraps the concrete `doSend` implementation in a retry loop.
76
- * It tracks the last error encountered to provide context if all retries fail.
74
+ * Delegates to `doSend()` and retries on retryable InfrastructureException errors
75
+ * (CONNECTION_FAILED, RATE_LIMIT). Non-retryable errors surface immediately.
77
76
  *
78
77
  * @param message - The message to be delivered.
79
78
  * @returns A promise that resolves when the message is successfully sent.
80
- * @throws {MailTransportError} If the message cannot be sent after the maximum number of retries.
79
+ * @throws {MailTransportError} If the message cannot be sent after the maximum number of attempts.
81
80
  *
82
81
  * @example
83
82
  * ```typescript
@@ -94,18 +93,11 @@ export declare abstract class BaseTransport implements Transport {
94
93
  * Actual transport implementation to be provided by subclasses.
95
94
  *
96
95
  * This method should contain the protocol-specific logic for delivering the message.
97
- * It will be automatically retried by the `send` method if it throws an error.
96
+ * It will be automatically retried by `send()` if it throws a retryable error.
98
97
  *
99
98
  * @param message - The message to send.
100
99
  * @returns A promise that resolves when the delivery is successful.
101
- * @throws {Error} Any error encountered during delivery, which will trigger a retry.
100
+ * @throws {Error} Any error encountered during delivery, which may trigger a retry.
102
101
  */
103
102
  protected abstract doSend(message: Message): Promise<void>;
104
- /**
105
- * Utility method to pause execution for a given duration.
106
- *
107
- * @param ms - Milliseconds to sleep.
108
- * @returns A promise that resolves after the delay.
109
- */
110
- private sleep;
111
103
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gravito/signal",
3
3
  "sideEffects": false,
4
- "version": "3.1.2",
4
+ "version": "4.0.0",
5
5
  "description": "Powerful email framework for Gravito applications with Dev UI and multi-renderer support.",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -39,12 +39,13 @@
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
41
  "@aws-sdk/client-ses": "^3.953.0",
42
+ "@gravito/resilience": "workspace:*",
42
43
  "nodemailer": "^7.0.11"
43
44
  },
44
45
  "peerDependencies": {
45
- "@gravito/core": "^2.0.0",
46
- "@gravito/stream": "^2.1.0",
47
- "@gravito/prism": "^3.1.1",
46
+ "@gravito/core": "^3.0.0",
47
+ "@gravito/stream": "^3.0.0",
48
+ "@gravito/prism": "^4.0.0",
48
49
  "react": "^19.0.0",
49
50
  "react-dom": "^19.0.0",
50
51
  "vue": "^3.0.0"