@upyo/core 0.5.0-dev.86 → 0.5.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.
- package/README.md +35 -5
- package/dist/abort-signal.cjs +53 -0
- package/dist/abort-signal.d.cts +32 -0
- package/dist/abort-signal.d.ts +32 -0
- package/dist/abort-signal.js +52 -0
- package/dist/index.cjs +9 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -1
- package/dist/receipt.cjs +184 -0
- package/dist/receipt.d.cts +161 -2
- package/dist/receipt.d.ts +161 -2
- package/dist/receipt.js +179 -0
- package/dist/transport.d.cts +10 -4
- package/dist/transport.d.ts +10 -4
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -108,27 +108,57 @@ function handleReceipt(receipt: Receipt) {
|
|
|
108
108
|
console.log("Message sent with ID:", receipt.messageId);
|
|
109
109
|
} else {
|
|
110
110
|
console.error("Send failed:", receipt.errorMessages.join(", "));
|
|
111
|
+
console.error("Retryable:", receipt.retryable ?? false);
|
|
112
|
+
|
|
113
|
+
for (const error of receipt.errors ?? []) {
|
|
114
|
+
console.error(error.category, error.code, error.provider);
|
|
115
|
+
}
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
~~~~
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
Failed receipts keep the legacy `errorMessages` array and can also carry
|
|
121
|
+
structured `errors` for programmatic handling. Transports use categories such
|
|
122
|
+
as `auth`, `rate-limit`, `network`, `timeout`, `validation`, `rejected`,
|
|
123
|
+
`server-error`, `service-unavailable`, `configuration`, and `unknown`.
|
|
124
|
+
|
|
125
|
+
When implementing a transport, use `createFailedReceipt()` to keep these fields
|
|
126
|
+
consistent:
|
|
127
|
+
|
|
128
|
+
~~~~ typescript
|
|
129
|
+
import { createFailedReceipt } from "@upyo/core";
|
|
130
|
+
|
|
131
|
+
const receipt = createFailedReceipt("HTTP 429: Too Many Requests", {
|
|
132
|
+
provider: "example",
|
|
133
|
+
statusCode: 429,
|
|
134
|
+
retryAfterMilliseconds: 30_000,
|
|
135
|
+
});
|
|
136
|
+
~~~~
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
Implementing custom transports
|
|
140
|
+
------------------------------
|
|
116
141
|
|
|
117
142
|
The `Transport` interface defines the contract for all email providers:
|
|
118
143
|
|
|
119
144
|
~~~~ typescript
|
|
120
145
|
import type { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
121
146
|
|
|
122
|
-
class MyCustomTransport implements Transport {
|
|
123
|
-
|
|
147
|
+
class MyCustomTransport implements Transport<"example"> {
|
|
148
|
+
readonly id = "example";
|
|
149
|
+
|
|
150
|
+
async send(
|
|
151
|
+
message: Message,
|
|
152
|
+
options?: TransportOptions,
|
|
153
|
+
): Promise<Receipt<"example">> {
|
|
124
154
|
// Implementation details...
|
|
125
|
-
return { successful: true, messageId: "12345" };
|
|
155
|
+
return { successful: true, messageId: "12345", provider: this.id };
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
async *sendMany(
|
|
129
159
|
messages: Iterable<Message> | AsyncIterable<Message>,
|
|
130
160
|
options?: TransportOptions,
|
|
131
|
-
): AsyncIterable<Receipt
|
|
161
|
+
): AsyncIterable<Receipt<"example">> {
|
|
132
162
|
for await (const message of messages) {
|
|
133
163
|
yield await this.send(message, options);
|
|
134
164
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/abort-signal.ts
|
|
3
|
+
/**
|
|
4
|
+
* Combines a primary abort signal with an optional external signal.
|
|
5
|
+
*
|
|
6
|
+
* The returned signal aborts as soon as either input signal aborts. Runtimes
|
|
7
|
+
* with `AbortSignal.any()` use the platform implementation; older runtimes use
|
|
8
|
+
* a listener-based fallback that preserves the original abort reason.
|
|
9
|
+
*
|
|
10
|
+
* @param primarySignal The signal owned by the current operation.
|
|
11
|
+
* @param externalSignal Optional caller-supplied signal to compose.
|
|
12
|
+
* @returns A combined signal and cleanup callback.
|
|
13
|
+
* @since 0.5.0
|
|
14
|
+
*/
|
|
15
|
+
function combineSignals(primarySignal, externalSignal) {
|
|
16
|
+
if (externalSignal == null) return {
|
|
17
|
+
signal: primarySignal,
|
|
18
|
+
cleanup: () => {}
|
|
19
|
+
};
|
|
20
|
+
if (typeof AbortSignal.any === "function") return {
|
|
21
|
+
signal: AbortSignal.any([primarySignal, externalSignal]),
|
|
22
|
+
cleanup: () => {}
|
|
23
|
+
};
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const cleanup = () => {
|
|
26
|
+
primarySignal.removeEventListener("abort", abortFromPrimary);
|
|
27
|
+
externalSignal.removeEventListener("abort", abortFromExternal);
|
|
28
|
+
};
|
|
29
|
+
const abortFromPrimary = () => {
|
|
30
|
+
cleanup();
|
|
31
|
+
controller.abort(getAbortReason(primarySignal));
|
|
32
|
+
};
|
|
33
|
+
const abortFromExternal = () => {
|
|
34
|
+
cleanup();
|
|
35
|
+
controller.abort(getAbortReason(externalSignal));
|
|
36
|
+
};
|
|
37
|
+
if (primarySignal.aborted) controller.abort(getAbortReason(primarySignal));
|
|
38
|
+
else if (externalSignal.aborted) controller.abort(getAbortReason(externalSignal));
|
|
39
|
+
else {
|
|
40
|
+
primarySignal.addEventListener("abort", abortFromPrimary, { once: true });
|
|
41
|
+
externalSignal.addEventListener("abort", abortFromExternal, { once: true });
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
cleanup
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getAbortReason(signal) {
|
|
49
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
exports.combineSignals = combineSignals;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/abort-signal.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Result of combining abort signals.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.5.0
|
|
6
|
+
*/
|
|
7
|
+
interface CombinedSignal {
|
|
8
|
+
/**
|
|
9
|
+
* The signal that aborts when any input signal aborts.
|
|
10
|
+
*/
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
/**
|
|
13
|
+
* Removes fallback abort listeners when signal composition no longer needs
|
|
14
|
+
* them.
|
|
15
|
+
*/
|
|
16
|
+
cleanup(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Combines a primary abort signal with an optional external signal.
|
|
20
|
+
*
|
|
21
|
+
* The returned signal aborts as soon as either input signal aborts. Runtimes
|
|
22
|
+
* with `AbortSignal.any()` use the platform implementation; older runtimes use
|
|
23
|
+
* a listener-based fallback that preserves the original abort reason.
|
|
24
|
+
*
|
|
25
|
+
* @param primarySignal The signal owned by the current operation.
|
|
26
|
+
* @param externalSignal Optional caller-supplied signal to compose.
|
|
27
|
+
* @returns A combined signal and cleanup callback.
|
|
28
|
+
* @since 0.5.0
|
|
29
|
+
*/
|
|
30
|
+
declare function combineSignals(primarySignal: AbortSignal, externalSignal?: AbortSignal | null): CombinedSignal;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { CombinedSignal, combineSignals };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/abort-signal.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Result of combining abort signals.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.5.0
|
|
6
|
+
*/
|
|
7
|
+
interface CombinedSignal {
|
|
8
|
+
/**
|
|
9
|
+
* The signal that aborts when any input signal aborts.
|
|
10
|
+
*/
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
/**
|
|
13
|
+
* Removes fallback abort listeners when signal composition no longer needs
|
|
14
|
+
* them.
|
|
15
|
+
*/
|
|
16
|
+
cleanup(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Combines a primary abort signal with an optional external signal.
|
|
20
|
+
*
|
|
21
|
+
* The returned signal aborts as soon as either input signal aborts. Runtimes
|
|
22
|
+
* with `AbortSignal.any()` use the platform implementation; older runtimes use
|
|
23
|
+
* a listener-based fallback that preserves the original abort reason.
|
|
24
|
+
*
|
|
25
|
+
* @param primarySignal The signal owned by the current operation.
|
|
26
|
+
* @param externalSignal Optional caller-supplied signal to compose.
|
|
27
|
+
* @returns A combined signal and cleanup callback.
|
|
28
|
+
* @since 0.5.0
|
|
29
|
+
*/
|
|
30
|
+
declare function combineSignals(primarySignal: AbortSignal, externalSignal?: AbortSignal | null): CombinedSignal;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { CombinedSignal, combineSignals };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/abort-signal.ts
|
|
2
|
+
/**
|
|
3
|
+
* Combines a primary abort signal with an optional external signal.
|
|
4
|
+
*
|
|
5
|
+
* The returned signal aborts as soon as either input signal aborts. Runtimes
|
|
6
|
+
* with `AbortSignal.any()` use the platform implementation; older runtimes use
|
|
7
|
+
* a listener-based fallback that preserves the original abort reason.
|
|
8
|
+
*
|
|
9
|
+
* @param primarySignal The signal owned by the current operation.
|
|
10
|
+
* @param externalSignal Optional caller-supplied signal to compose.
|
|
11
|
+
* @returns A combined signal and cleanup callback.
|
|
12
|
+
* @since 0.5.0
|
|
13
|
+
*/
|
|
14
|
+
function combineSignals(primarySignal, externalSignal) {
|
|
15
|
+
if (externalSignal == null) return {
|
|
16
|
+
signal: primarySignal,
|
|
17
|
+
cleanup: () => {}
|
|
18
|
+
};
|
|
19
|
+
if (typeof AbortSignal.any === "function") return {
|
|
20
|
+
signal: AbortSignal.any([primarySignal, externalSignal]),
|
|
21
|
+
cleanup: () => {}
|
|
22
|
+
};
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
primarySignal.removeEventListener("abort", abortFromPrimary);
|
|
26
|
+
externalSignal.removeEventListener("abort", abortFromExternal);
|
|
27
|
+
};
|
|
28
|
+
const abortFromPrimary = () => {
|
|
29
|
+
cleanup();
|
|
30
|
+
controller.abort(getAbortReason(primarySignal));
|
|
31
|
+
};
|
|
32
|
+
const abortFromExternal = () => {
|
|
33
|
+
cleanup();
|
|
34
|
+
controller.abort(getAbortReason(externalSignal));
|
|
35
|
+
};
|
|
36
|
+
if (primarySignal.aborted) controller.abort(getAbortReason(primarySignal));
|
|
37
|
+
else if (externalSignal.aborted) controller.abort(getAbortReason(externalSignal));
|
|
38
|
+
else {
|
|
39
|
+
primarySignal.addEventListener("abort", abortFromPrimary, { once: true });
|
|
40
|
+
externalSignal.addEventListener("abort", abortFromExternal, { once: true });
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
cleanup
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function getAbortReason(signal) {
|
|
48
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { combineSignals };
|
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
const require_abort_signal = require('./abort-signal.cjs');
|
|
1
2
|
const require_address = require('./address.cjs');
|
|
2
3
|
const require_attachment = require('./attachment.cjs');
|
|
3
4
|
const require_message = require('./message.cjs');
|
|
4
5
|
const require_priority = require('./priority.cjs');
|
|
6
|
+
const require_receipt = require('./receipt.cjs');
|
|
5
7
|
|
|
8
|
+
exports.classifyHttpStatus = require_receipt.classifyHttpStatus;
|
|
9
|
+
exports.classifyReceiptError = require_receipt.classifyReceiptError;
|
|
10
|
+
exports.combineSignals = require_abort_signal.combineSignals;
|
|
6
11
|
exports.comparePriority = require_priority.comparePriority;
|
|
12
|
+
exports.createFailedReceipt = require_receipt.createFailedReceipt;
|
|
7
13
|
exports.createMessage = require_message.createMessage;
|
|
14
|
+
exports.createReceiptError = require_receipt.createReceiptError;
|
|
8
15
|
exports.formatAddress = require_address.formatAddress;
|
|
9
16
|
exports.isAttachment = require_attachment.isAttachment;
|
|
10
17
|
exports.isEmailAddress = require_address.isEmailAddress;
|
|
11
|
-
exports.parseAddress = require_address.parseAddress;
|
|
18
|
+
exports.parseAddress = require_address.parseAddress;
|
|
19
|
+
exports.parseRetryAfter = require_receipt.parseRetryAfter;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { CombinedSignal, combineSignals } from "./abort-signal.cjs";
|
|
1
2
|
import { Address, EmailAddress, formatAddress, isEmailAddress, parseAddress } from "./address.cjs";
|
|
2
3
|
import { Attachment, isAttachment } from "./attachment.cjs";
|
|
3
4
|
import { Priority, comparePriority } from "./priority.cjs";
|
|
4
5
|
import { ImmutableHeaders, Message, MessageConstructor, MessageContent, createMessage } from "./message.cjs";
|
|
5
|
-
import { Receipt } from "./receipt.cjs";
|
|
6
|
+
import { CreateFailedReceiptOptions, CreateReceiptErrorOptions, Receipt, ReceiptError, ReceiptErrorCategory, ReceiptErrorClassification, classifyHttpStatus, classifyReceiptError, createFailedReceipt, createReceiptError, parseRetryAfter } from "./receipt.cjs";
|
|
6
7
|
import { Transport, TransportOptions } from "./transport.cjs";
|
|
7
|
-
export { Address, Attachment, EmailAddress, ImmutableHeaders, Message, MessageConstructor, MessageContent, Priority, Receipt, Transport, TransportOptions, comparePriority, createMessage, formatAddress, isAttachment, isEmailAddress, parseAddress };
|
|
8
|
+
export { Address, Attachment, CombinedSignal, CreateFailedReceiptOptions, CreateReceiptErrorOptions, EmailAddress, ImmutableHeaders, Message, MessageConstructor, MessageContent, Priority, Receipt, ReceiptError, ReceiptErrorCategory, ReceiptErrorClassification, Transport, TransportOptions, classifyHttpStatus, classifyReceiptError, combineSignals, comparePriority, createFailedReceipt, createMessage, createReceiptError, formatAddress, isAttachment, isEmailAddress, parseAddress, parseRetryAfter };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { CombinedSignal, combineSignals } from "./abort-signal.js";
|
|
1
2
|
import { Address, EmailAddress, formatAddress, isEmailAddress, parseAddress } from "./address.js";
|
|
2
3
|
import { Attachment, isAttachment } from "./attachment.js";
|
|
3
4
|
import { Priority, comparePriority } from "./priority.js";
|
|
4
5
|
import { ImmutableHeaders, Message, MessageConstructor, MessageContent, createMessage } from "./message.js";
|
|
5
|
-
import { Receipt } from "./receipt.js";
|
|
6
|
+
import { CreateFailedReceiptOptions, CreateReceiptErrorOptions, Receipt, ReceiptError, ReceiptErrorCategory, ReceiptErrorClassification, classifyHttpStatus, classifyReceiptError, createFailedReceipt, createReceiptError, parseRetryAfter } from "./receipt.js";
|
|
6
7
|
import { Transport, TransportOptions } from "./transport.js";
|
|
7
|
-
export { Address, Attachment, EmailAddress, ImmutableHeaders, Message, MessageConstructor, MessageContent, Priority, Receipt, Transport, TransportOptions, comparePriority, createMessage, formatAddress, isAttachment, isEmailAddress, parseAddress };
|
|
8
|
+
export { Address, Attachment, CombinedSignal, CreateFailedReceiptOptions, CreateReceiptErrorOptions, EmailAddress, ImmutableHeaders, Message, MessageConstructor, MessageContent, Priority, Receipt, ReceiptError, ReceiptErrorCategory, ReceiptErrorClassification, Transport, TransportOptions, classifyHttpStatus, classifyReceiptError, combineSignals, comparePriority, createFailedReceipt, createMessage, createReceiptError, formatAddress, isAttachment, isEmailAddress, parseAddress, parseRetryAfter };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { combineSignals } from "./abort-signal.js";
|
|
1
2
|
import { formatAddress, isEmailAddress, parseAddress } from "./address.js";
|
|
2
3
|
import { isAttachment } from "./attachment.js";
|
|
3
4
|
import { createMessage } from "./message.js";
|
|
4
5
|
import { comparePriority } from "./priority.js";
|
|
6
|
+
import { classifyHttpStatus, classifyReceiptError, createFailedReceipt, createReceiptError, parseRetryAfter } from "./receipt.js";
|
|
5
7
|
|
|
6
|
-
export { comparePriority, createMessage, formatAddress, isAttachment, isEmailAddress, parseAddress };
|
|
8
|
+
export { classifyHttpStatus, classifyReceiptError, combineSignals, comparePriority, createFailedReceipt, createMessage, createReceiptError, formatAddress, isAttachment, isEmailAddress, parseAddress, parseRetryAfter };
|
package/dist/receipt.cjs
CHANGED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/receipt.ts
|
|
3
|
+
/**
|
|
4
|
+
* Classifies an HTTP status code for delivery error handling.
|
|
5
|
+
*
|
|
6
|
+
* @param statusCode The HTTP status code to classify.
|
|
7
|
+
* @returns The receipt error category and retryability.
|
|
8
|
+
* @since 0.5.0
|
|
9
|
+
*/
|
|
10
|
+
function classifyHttpStatus(statusCode) {
|
|
11
|
+
if (statusCode === 401 || statusCode === 403) return {
|
|
12
|
+
category: "auth",
|
|
13
|
+
retryable: false
|
|
14
|
+
};
|
|
15
|
+
if (statusCode === 408 || statusCode === 504) return {
|
|
16
|
+
category: "timeout",
|
|
17
|
+
retryable: true
|
|
18
|
+
};
|
|
19
|
+
if (statusCode === 429) return {
|
|
20
|
+
category: "rate-limit",
|
|
21
|
+
retryable: true
|
|
22
|
+
};
|
|
23
|
+
if (statusCode === 503) return {
|
|
24
|
+
category: "service-unavailable",
|
|
25
|
+
retryable: true
|
|
26
|
+
};
|
|
27
|
+
if (statusCode >= 400 && statusCode < 500) return {
|
|
28
|
+
category: "validation",
|
|
29
|
+
retryable: false
|
|
30
|
+
};
|
|
31
|
+
if (statusCode >= 500 && statusCode < 600) return {
|
|
32
|
+
category: "server-error",
|
|
33
|
+
retryable: true
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
category: "unknown",
|
|
37
|
+
retryable: false
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Classifies an arbitrary delivery failure.
|
|
42
|
+
*
|
|
43
|
+
* @param error The error to classify.
|
|
44
|
+
* @returns The receipt error category, retryability, and default code.
|
|
45
|
+
* @since 0.5.0
|
|
46
|
+
*/
|
|
47
|
+
function classifyReceiptError(error) {
|
|
48
|
+
const message = getStringProperty(error, "message") ?? String(error);
|
|
49
|
+
const name = getStringProperty(error, "name") ?? "";
|
|
50
|
+
const text = `${name} ${message}`.toLowerCase();
|
|
51
|
+
if (text.includes("timeout") || text.includes("timed out")) return {
|
|
52
|
+
category: "timeout",
|
|
53
|
+
retryable: true,
|
|
54
|
+
code: "timeout"
|
|
55
|
+
};
|
|
56
|
+
if (text.includes("network") || text.includes("connect") || text.includes("fetch failed") || text.includes("dns") || text.includes("econnreset") || text.includes("econnrefused") || text.includes("enotfound") || name === "NetworkError") return {
|
|
57
|
+
category: "network",
|
|
58
|
+
retryable: true,
|
|
59
|
+
code: "network"
|
|
60
|
+
};
|
|
61
|
+
if (text.includes("authentication") || text.includes("authenticate") || text.includes("authorization") || text.includes("authorize") || /\bauth\b/.test(text) || text.includes("unauthorized") || text.includes("not authorized") || text.includes("forbidden") || text.includes("invalid api key") || text.includes("invalid token") || text.includes("401") || text.includes("403")) return {
|
|
62
|
+
category: "auth",
|
|
63
|
+
retryable: false,
|
|
64
|
+
code: "auth"
|
|
65
|
+
};
|
|
66
|
+
if (text.includes("rate limit") || text.includes("rate-limit") || text.includes("too many requests") || text.includes("quota") || text.includes("throttle") || text.includes("429")) return {
|
|
67
|
+
category: "rate-limit",
|
|
68
|
+
retryable: true,
|
|
69
|
+
code: "rate-limit"
|
|
70
|
+
};
|
|
71
|
+
if (text.includes("invalid") || text.includes("malformed") || text.includes("validation") || text.includes("bad request") || text.includes("400") || text.includes("422")) return {
|
|
72
|
+
category: "validation",
|
|
73
|
+
retryable: false,
|
|
74
|
+
code: "validation"
|
|
75
|
+
};
|
|
76
|
+
if (text.includes("rejected") || text.includes("bounce") || text.includes("complaint") || text.includes("suppressed") || text.includes("unsubscribed")) return {
|
|
77
|
+
category: "rejected",
|
|
78
|
+
retryable: false,
|
|
79
|
+
code: "rejected"
|
|
80
|
+
};
|
|
81
|
+
if (text.includes("service unavailable") || text.includes("temporarily unavailable") || text.includes("503")) return {
|
|
82
|
+
category: "service-unavailable",
|
|
83
|
+
retryable: true,
|
|
84
|
+
code: "service-unavailable"
|
|
85
|
+
};
|
|
86
|
+
if (text.includes("internal server error") || text.includes("bad gateway") || text.includes("500") || text.includes("502")) return {
|
|
87
|
+
category: "server-error",
|
|
88
|
+
retryable: true,
|
|
89
|
+
code: "server-error"
|
|
90
|
+
};
|
|
91
|
+
if (text.includes("config") || text.includes("unsupported")) return {
|
|
92
|
+
category: "configuration",
|
|
93
|
+
retryable: false,
|
|
94
|
+
code: "configuration"
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
category: "unknown",
|
|
98
|
+
retryable: false,
|
|
99
|
+
code: "unknown"
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Creates structured information about a delivery failure.
|
|
104
|
+
*
|
|
105
|
+
* @param message Human-readable error message.
|
|
106
|
+
* @param options Optional structured error metadata.
|
|
107
|
+
* @returns A structured receipt error.
|
|
108
|
+
* @since 0.5.0
|
|
109
|
+
*/
|
|
110
|
+
function createReceiptError(message, options = {}) {
|
|
111
|
+
const classification = options.statusCode == null ? classifyReceiptError(message) : classifyHttpStatus(options.statusCode);
|
|
112
|
+
const category = options.category ?? classification.category;
|
|
113
|
+
const retryable = options.retryable ?? classification.retryable;
|
|
114
|
+
const code = options.code ?? (options.statusCode == null ? options.category != null ? category : classification.code ?? category : `http.${options.statusCode}`);
|
|
115
|
+
return omitUndefined({
|
|
116
|
+
message,
|
|
117
|
+
category,
|
|
118
|
+
code,
|
|
119
|
+
retryable,
|
|
120
|
+
provider: options.provider,
|
|
121
|
+
statusCode: options.statusCode,
|
|
122
|
+
retryAfterMilliseconds: options.retryAfterMilliseconds,
|
|
123
|
+
providerDetails: options.providerDetails
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Creates a failed receipt with human-readable and structured errors.
|
|
128
|
+
*
|
|
129
|
+
* @param error Error message, structured error, or list of structured errors.
|
|
130
|
+
* @param options Optional receipt and error metadata.
|
|
131
|
+
* @returns A failed receipt.
|
|
132
|
+
* @since 0.5.0
|
|
133
|
+
*/
|
|
134
|
+
function createFailedReceipt(error, options = {}) {
|
|
135
|
+
const errors = options.errors ?? (typeof error === "string" ? [createReceiptError(error, options)] : Array.isArray(error) ? error.map((item) => typeof item === "string" ? createReceiptError(item, options) : item) : [error]);
|
|
136
|
+
const errorMessages = typeof error === "string" ? [error] : Array.isArray(error) ? error.map((item) => typeof item === "string" ? item : item.message) : errors.map((receiptError) => receiptError.message);
|
|
137
|
+
const retryable = options.retryable ?? errors.some((receiptError) => receiptError.retryable);
|
|
138
|
+
const provider = options.provider ?? errors[0]?.provider;
|
|
139
|
+
return omitUndefined({
|
|
140
|
+
successful: false,
|
|
141
|
+
errorMessages,
|
|
142
|
+
errors,
|
|
143
|
+
retryable,
|
|
144
|
+
provider,
|
|
145
|
+
attempts: options.attempts,
|
|
146
|
+
timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Parses an HTTP `Retry-After` header value into milliseconds.
|
|
151
|
+
*
|
|
152
|
+
* @param value Header value to parse.
|
|
153
|
+
* @param now Current time used when parsing HTTP dates.
|
|
154
|
+
* @returns Retry delay in milliseconds, or `undefined` if invalid.
|
|
155
|
+
* @since 0.5.0
|
|
156
|
+
*/
|
|
157
|
+
function parseRetryAfter(value, now = /* @__PURE__ */ new Date()) {
|
|
158
|
+
if (value == null || value.trim() === "") return void 0;
|
|
159
|
+
const trimmed = value.trim();
|
|
160
|
+
if (/^\d+$/.test(trimmed)) {
|
|
161
|
+
const delay$1 = Number(trimmed) * 1e3;
|
|
162
|
+
return delay$1;
|
|
163
|
+
}
|
|
164
|
+
const timestamp = Date.parse(trimmed);
|
|
165
|
+
if (Number.isNaN(timestamp)) return void 0;
|
|
166
|
+
const delay = timestamp - now.getTime();
|
|
167
|
+
return delay > 0 ? delay : void 0;
|
|
168
|
+
}
|
|
169
|
+
function getStringProperty(value, property) {
|
|
170
|
+
if (value instanceof Error) return value[property];
|
|
171
|
+
if (typeof value !== "object" || value == null || !(property in value)) return void 0;
|
|
172
|
+
const propertyValue = value[property];
|
|
173
|
+
return propertyValue == null ? void 0 : String(propertyValue);
|
|
174
|
+
}
|
|
175
|
+
function omitUndefined(value) {
|
|
176
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
exports.classifyHttpStatus = classifyHttpStatus;
|
|
181
|
+
exports.classifyReceiptError = classifyReceiptError;
|
|
182
|
+
exports.createFailedReceipt = createFailedReceipt;
|
|
183
|
+
exports.createReceiptError = createReceiptError;
|
|
184
|
+
exports.parseRetryAfter = parseRetryAfter;
|
package/dist/receipt.d.cts
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
//#region src/receipt.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A machine-readable category for an email delivery failure.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.5.0
|
|
6
|
+
*/
|
|
7
|
+
type ReceiptErrorCategory = "auth" | "rate-limit" | "network" | "timeout" | "validation" | "rejected" | "server-error" | "service-unavailable" | "configuration" | "unknown";
|
|
8
|
+
/**
|
|
9
|
+
* Structured information about an email delivery failure.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.5.0
|
|
12
|
+
*/
|
|
13
|
+
interface ReceiptError<TProviderId extends string = string> {
|
|
14
|
+
/**
|
|
15
|
+
* Human-readable error message.
|
|
16
|
+
*/
|
|
17
|
+
readonly message: string;
|
|
18
|
+
/**
|
|
19
|
+
* Machine-readable provider or protocol error code.
|
|
20
|
+
*/
|
|
21
|
+
readonly code: string;
|
|
22
|
+
/**
|
|
23
|
+
* Coarse error category for programmatic handling.
|
|
24
|
+
*/
|
|
25
|
+
readonly category: ReceiptErrorCategory;
|
|
26
|
+
/**
|
|
27
|
+
* Whether retrying the same message may succeed.
|
|
28
|
+
*/
|
|
29
|
+
readonly retryable: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Provider or transport that produced this error.
|
|
32
|
+
*/
|
|
33
|
+
readonly provider?: TProviderId;
|
|
34
|
+
/**
|
|
35
|
+
* HTTP status code, when the failure came from an HTTP API.
|
|
36
|
+
*/
|
|
37
|
+
readonly statusCode?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Provider-supplied retry delay in milliseconds, when available.
|
|
40
|
+
*/
|
|
41
|
+
readonly retryAfterMilliseconds?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Provider-specific error details, when available.
|
|
44
|
+
*/
|
|
45
|
+
readonly providerDetails?: unknown;
|
|
46
|
+
}
|
|
2
47
|
/**
|
|
3
48
|
* The response from the email service after sending an email message.
|
|
4
49
|
*
|
|
@@ -6,8 +51,11 @@
|
|
|
6
51
|
*
|
|
7
52
|
* - Successful sends have a `messageId` but no `errorMessages`
|
|
8
53
|
* - Failed sends have `errorMessages` but no `messageId`
|
|
54
|
+
*
|
|
55
|
+
* Failed receipts may also include structured `errors` and summary metadata
|
|
56
|
+
* for programmatic error handling.
|
|
9
57
|
*/
|
|
10
|
-
type Receipt = {
|
|
58
|
+
type Receipt<TProviderId extends string = string> = {
|
|
11
59
|
/**
|
|
12
60
|
* Indicates that the email was sent successfully.
|
|
13
61
|
*/
|
|
@@ -16,6 +64,18 @@ type Receipt = {
|
|
|
16
64
|
* The unique identifier for the message that was sent.
|
|
17
65
|
*/
|
|
18
66
|
readonly messageId: string;
|
|
67
|
+
/**
|
|
68
|
+
* Provider or transport that produced this receipt.
|
|
69
|
+
*/
|
|
70
|
+
readonly provider?: TProviderId;
|
|
71
|
+
/**
|
|
72
|
+
* Number of attempts made before this receipt was produced.
|
|
73
|
+
*/
|
|
74
|
+
readonly attempts?: number;
|
|
75
|
+
/**
|
|
76
|
+
* ISO 8601 timestamp for when this receipt was produced.
|
|
77
|
+
*/
|
|
78
|
+
readonly timestamp?: string;
|
|
19
79
|
} | {
|
|
20
80
|
/**
|
|
21
81
|
* Indicates that the email failed to send.
|
|
@@ -25,6 +85,105 @@ type Receipt = {
|
|
|
25
85
|
* An array of error messages that occurred during the sending process.
|
|
26
86
|
*/
|
|
27
87
|
readonly errorMessages: readonly string[];
|
|
88
|
+
/**
|
|
89
|
+
* Structured errors for programmatic handling.
|
|
90
|
+
*/
|
|
91
|
+
readonly errors?: readonly ReceiptError<TProviderId>[];
|
|
92
|
+
/**
|
|
93
|
+
* Whether retrying the same message may succeed.
|
|
94
|
+
*/
|
|
95
|
+
readonly retryable?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Provider or transport that produced this receipt.
|
|
98
|
+
*/
|
|
99
|
+
readonly provider?: TProviderId;
|
|
100
|
+
/**
|
|
101
|
+
* Number of attempts made before this receipt was produced.
|
|
102
|
+
*/
|
|
103
|
+
readonly attempts?: number;
|
|
104
|
+
/**
|
|
105
|
+
* ISO 8601 timestamp for when this receipt was produced.
|
|
106
|
+
*/
|
|
107
|
+
readonly timestamp?: string;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Result of classifying a delivery failure.
|
|
111
|
+
*
|
|
112
|
+
* @since 0.5.0
|
|
113
|
+
*/
|
|
114
|
+
interface ReceiptErrorClassification {
|
|
115
|
+
readonly category: ReceiptErrorCategory;
|
|
116
|
+
readonly retryable: boolean;
|
|
117
|
+
readonly code?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Options for creating a structured receipt error.
|
|
121
|
+
*
|
|
122
|
+
* @since 0.5.0
|
|
123
|
+
*/
|
|
124
|
+
interface CreateReceiptErrorOptions<TProviderId extends string = string> {
|
|
125
|
+
readonly code?: string;
|
|
126
|
+
readonly category?: ReceiptErrorCategory;
|
|
127
|
+
readonly retryable?: boolean;
|
|
128
|
+
readonly provider?: TProviderId;
|
|
129
|
+
readonly statusCode?: number;
|
|
130
|
+
readonly retryAfterMilliseconds?: number;
|
|
131
|
+
readonly providerDetails?: unknown;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Options for creating a failed receipt.
|
|
135
|
+
*
|
|
136
|
+
* @since 0.5.0
|
|
137
|
+
*/
|
|
138
|
+
interface CreateFailedReceiptOptions<TProviderId extends string = string> extends CreateReceiptErrorOptions<TProviderId> {
|
|
139
|
+
readonly errors?: readonly ReceiptError<TProviderId>[];
|
|
140
|
+
readonly attempts?: number;
|
|
141
|
+
readonly timestamp?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Classifies an HTTP status code for delivery error handling.
|
|
145
|
+
*
|
|
146
|
+
* @param statusCode The HTTP status code to classify.
|
|
147
|
+
* @returns The receipt error category and retryability.
|
|
148
|
+
* @since 0.5.0
|
|
149
|
+
*/
|
|
150
|
+
declare function classifyHttpStatus(statusCode: number): ReceiptErrorClassification;
|
|
151
|
+
/**
|
|
152
|
+
* Classifies an arbitrary delivery failure.
|
|
153
|
+
*
|
|
154
|
+
* @param error The error to classify.
|
|
155
|
+
* @returns The receipt error category, retryability, and default code.
|
|
156
|
+
* @since 0.5.0
|
|
157
|
+
*/
|
|
158
|
+
declare function classifyReceiptError(error: unknown): Required<ReceiptErrorClassification>;
|
|
159
|
+
/**
|
|
160
|
+
* Creates structured information about a delivery failure.
|
|
161
|
+
*
|
|
162
|
+
* @param message Human-readable error message.
|
|
163
|
+
* @param options Optional structured error metadata.
|
|
164
|
+
* @returns A structured receipt error.
|
|
165
|
+
* @since 0.5.0
|
|
166
|
+
*/
|
|
167
|
+
declare function createReceiptError<TProviderId extends string = string>(message: string, options?: CreateReceiptErrorOptions<TProviderId>): ReceiptError<TProviderId>;
|
|
168
|
+
/**
|
|
169
|
+
* Creates a failed receipt with human-readable and structured errors.
|
|
170
|
+
*
|
|
171
|
+
* @param error Error message, structured error, or list of structured errors.
|
|
172
|
+
* @param options Optional receipt and error metadata.
|
|
173
|
+
* @returns A failed receipt.
|
|
174
|
+
* @since 0.5.0
|
|
175
|
+
*/
|
|
176
|
+
declare function createFailedReceipt<TProviderId extends string = string>(error: string | readonly (string | ReceiptError<TProviderId>)[] | ReceiptError<TProviderId>, options?: CreateFailedReceiptOptions<TProviderId>): Receipt<TProviderId> & {
|
|
177
|
+
readonly successful: false;
|
|
28
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* Parses an HTTP `Retry-After` header value into milliseconds.
|
|
181
|
+
*
|
|
182
|
+
* @param value Header value to parse.
|
|
183
|
+
* @param now Current time used when parsing HTTP dates.
|
|
184
|
+
* @returns Retry delay in milliseconds, or `undefined` if invalid.
|
|
185
|
+
* @since 0.5.0
|
|
186
|
+
*/
|
|
187
|
+
declare function parseRetryAfter(value: string | null | undefined, now?: Date): number | undefined;
|
|
29
188
|
//#endregion
|
|
30
|
-
export { Receipt };
|
|
189
|
+
export { CreateFailedReceiptOptions, CreateReceiptErrorOptions, Receipt, ReceiptError, ReceiptErrorCategory, ReceiptErrorClassification, classifyHttpStatus, classifyReceiptError, createFailedReceipt, createReceiptError, parseRetryAfter };
|
package/dist/receipt.d.ts
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
//#region src/receipt.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A machine-readable category for an email delivery failure.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.5.0
|
|
6
|
+
*/
|
|
7
|
+
type ReceiptErrorCategory = "auth" | "rate-limit" | "network" | "timeout" | "validation" | "rejected" | "server-error" | "service-unavailable" | "configuration" | "unknown";
|
|
8
|
+
/**
|
|
9
|
+
* Structured information about an email delivery failure.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.5.0
|
|
12
|
+
*/
|
|
13
|
+
interface ReceiptError<TProviderId extends string = string> {
|
|
14
|
+
/**
|
|
15
|
+
* Human-readable error message.
|
|
16
|
+
*/
|
|
17
|
+
readonly message: string;
|
|
18
|
+
/**
|
|
19
|
+
* Machine-readable provider or protocol error code.
|
|
20
|
+
*/
|
|
21
|
+
readonly code: string;
|
|
22
|
+
/**
|
|
23
|
+
* Coarse error category for programmatic handling.
|
|
24
|
+
*/
|
|
25
|
+
readonly category: ReceiptErrorCategory;
|
|
26
|
+
/**
|
|
27
|
+
* Whether retrying the same message may succeed.
|
|
28
|
+
*/
|
|
29
|
+
readonly retryable: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Provider or transport that produced this error.
|
|
32
|
+
*/
|
|
33
|
+
readonly provider?: TProviderId;
|
|
34
|
+
/**
|
|
35
|
+
* HTTP status code, when the failure came from an HTTP API.
|
|
36
|
+
*/
|
|
37
|
+
readonly statusCode?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Provider-supplied retry delay in milliseconds, when available.
|
|
40
|
+
*/
|
|
41
|
+
readonly retryAfterMilliseconds?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Provider-specific error details, when available.
|
|
44
|
+
*/
|
|
45
|
+
readonly providerDetails?: unknown;
|
|
46
|
+
}
|
|
2
47
|
/**
|
|
3
48
|
* The response from the email service after sending an email message.
|
|
4
49
|
*
|
|
@@ -6,8 +51,11 @@
|
|
|
6
51
|
*
|
|
7
52
|
* - Successful sends have a `messageId` but no `errorMessages`
|
|
8
53
|
* - Failed sends have `errorMessages` but no `messageId`
|
|
54
|
+
*
|
|
55
|
+
* Failed receipts may also include structured `errors` and summary metadata
|
|
56
|
+
* for programmatic error handling.
|
|
9
57
|
*/
|
|
10
|
-
type Receipt = {
|
|
58
|
+
type Receipt<TProviderId extends string = string> = {
|
|
11
59
|
/**
|
|
12
60
|
* Indicates that the email was sent successfully.
|
|
13
61
|
*/
|
|
@@ -16,6 +64,18 @@ type Receipt = {
|
|
|
16
64
|
* The unique identifier for the message that was sent.
|
|
17
65
|
*/
|
|
18
66
|
readonly messageId: string;
|
|
67
|
+
/**
|
|
68
|
+
* Provider or transport that produced this receipt.
|
|
69
|
+
*/
|
|
70
|
+
readonly provider?: TProviderId;
|
|
71
|
+
/**
|
|
72
|
+
* Number of attempts made before this receipt was produced.
|
|
73
|
+
*/
|
|
74
|
+
readonly attempts?: number;
|
|
75
|
+
/**
|
|
76
|
+
* ISO 8601 timestamp for when this receipt was produced.
|
|
77
|
+
*/
|
|
78
|
+
readonly timestamp?: string;
|
|
19
79
|
} | {
|
|
20
80
|
/**
|
|
21
81
|
* Indicates that the email failed to send.
|
|
@@ -25,6 +85,105 @@ type Receipt = {
|
|
|
25
85
|
* An array of error messages that occurred during the sending process.
|
|
26
86
|
*/
|
|
27
87
|
readonly errorMessages: readonly string[];
|
|
88
|
+
/**
|
|
89
|
+
* Structured errors for programmatic handling.
|
|
90
|
+
*/
|
|
91
|
+
readonly errors?: readonly ReceiptError<TProviderId>[];
|
|
92
|
+
/**
|
|
93
|
+
* Whether retrying the same message may succeed.
|
|
94
|
+
*/
|
|
95
|
+
readonly retryable?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Provider or transport that produced this receipt.
|
|
98
|
+
*/
|
|
99
|
+
readonly provider?: TProviderId;
|
|
100
|
+
/**
|
|
101
|
+
* Number of attempts made before this receipt was produced.
|
|
102
|
+
*/
|
|
103
|
+
readonly attempts?: number;
|
|
104
|
+
/**
|
|
105
|
+
* ISO 8601 timestamp for when this receipt was produced.
|
|
106
|
+
*/
|
|
107
|
+
readonly timestamp?: string;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Result of classifying a delivery failure.
|
|
111
|
+
*
|
|
112
|
+
* @since 0.5.0
|
|
113
|
+
*/
|
|
114
|
+
interface ReceiptErrorClassification {
|
|
115
|
+
readonly category: ReceiptErrorCategory;
|
|
116
|
+
readonly retryable: boolean;
|
|
117
|
+
readonly code?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Options for creating a structured receipt error.
|
|
121
|
+
*
|
|
122
|
+
* @since 0.5.0
|
|
123
|
+
*/
|
|
124
|
+
interface CreateReceiptErrorOptions<TProviderId extends string = string> {
|
|
125
|
+
readonly code?: string;
|
|
126
|
+
readonly category?: ReceiptErrorCategory;
|
|
127
|
+
readonly retryable?: boolean;
|
|
128
|
+
readonly provider?: TProviderId;
|
|
129
|
+
readonly statusCode?: number;
|
|
130
|
+
readonly retryAfterMilliseconds?: number;
|
|
131
|
+
readonly providerDetails?: unknown;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Options for creating a failed receipt.
|
|
135
|
+
*
|
|
136
|
+
* @since 0.5.0
|
|
137
|
+
*/
|
|
138
|
+
interface CreateFailedReceiptOptions<TProviderId extends string = string> extends CreateReceiptErrorOptions<TProviderId> {
|
|
139
|
+
readonly errors?: readonly ReceiptError<TProviderId>[];
|
|
140
|
+
readonly attempts?: number;
|
|
141
|
+
readonly timestamp?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Classifies an HTTP status code for delivery error handling.
|
|
145
|
+
*
|
|
146
|
+
* @param statusCode The HTTP status code to classify.
|
|
147
|
+
* @returns The receipt error category and retryability.
|
|
148
|
+
* @since 0.5.0
|
|
149
|
+
*/
|
|
150
|
+
declare function classifyHttpStatus(statusCode: number): ReceiptErrorClassification;
|
|
151
|
+
/**
|
|
152
|
+
* Classifies an arbitrary delivery failure.
|
|
153
|
+
*
|
|
154
|
+
* @param error The error to classify.
|
|
155
|
+
* @returns The receipt error category, retryability, and default code.
|
|
156
|
+
* @since 0.5.0
|
|
157
|
+
*/
|
|
158
|
+
declare function classifyReceiptError(error: unknown): Required<ReceiptErrorClassification>;
|
|
159
|
+
/**
|
|
160
|
+
* Creates structured information about a delivery failure.
|
|
161
|
+
*
|
|
162
|
+
* @param message Human-readable error message.
|
|
163
|
+
* @param options Optional structured error metadata.
|
|
164
|
+
* @returns A structured receipt error.
|
|
165
|
+
* @since 0.5.0
|
|
166
|
+
*/
|
|
167
|
+
declare function createReceiptError<TProviderId extends string = string>(message: string, options?: CreateReceiptErrorOptions<TProviderId>): ReceiptError<TProviderId>;
|
|
168
|
+
/**
|
|
169
|
+
* Creates a failed receipt with human-readable and structured errors.
|
|
170
|
+
*
|
|
171
|
+
* @param error Error message, structured error, or list of structured errors.
|
|
172
|
+
* @param options Optional receipt and error metadata.
|
|
173
|
+
* @returns A failed receipt.
|
|
174
|
+
* @since 0.5.0
|
|
175
|
+
*/
|
|
176
|
+
declare function createFailedReceipt<TProviderId extends string = string>(error: string | readonly (string | ReceiptError<TProviderId>)[] | ReceiptError<TProviderId>, options?: CreateFailedReceiptOptions<TProviderId>): Receipt<TProviderId> & {
|
|
177
|
+
readonly successful: false;
|
|
28
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* Parses an HTTP `Retry-After` header value into milliseconds.
|
|
181
|
+
*
|
|
182
|
+
* @param value Header value to parse.
|
|
183
|
+
* @param now Current time used when parsing HTTP dates.
|
|
184
|
+
* @returns Retry delay in milliseconds, or `undefined` if invalid.
|
|
185
|
+
* @since 0.5.0
|
|
186
|
+
*/
|
|
187
|
+
declare function parseRetryAfter(value: string | null | undefined, now?: Date): number | undefined;
|
|
29
188
|
//#endregion
|
|
30
|
-
export { Receipt };
|
|
189
|
+
export { CreateFailedReceiptOptions, CreateReceiptErrorOptions, Receipt, ReceiptError, ReceiptErrorCategory, ReceiptErrorClassification, classifyHttpStatus, classifyReceiptError, createFailedReceipt, createReceiptError, parseRetryAfter };
|
package/dist/receipt.js
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
//#region src/receipt.ts
|
|
2
|
+
/**
|
|
3
|
+
* Classifies an HTTP status code for delivery error handling.
|
|
4
|
+
*
|
|
5
|
+
* @param statusCode The HTTP status code to classify.
|
|
6
|
+
* @returns The receipt error category and retryability.
|
|
7
|
+
* @since 0.5.0
|
|
8
|
+
*/
|
|
9
|
+
function classifyHttpStatus(statusCode) {
|
|
10
|
+
if (statusCode === 401 || statusCode === 403) return {
|
|
11
|
+
category: "auth",
|
|
12
|
+
retryable: false
|
|
13
|
+
};
|
|
14
|
+
if (statusCode === 408 || statusCode === 504) return {
|
|
15
|
+
category: "timeout",
|
|
16
|
+
retryable: true
|
|
17
|
+
};
|
|
18
|
+
if (statusCode === 429) return {
|
|
19
|
+
category: "rate-limit",
|
|
20
|
+
retryable: true
|
|
21
|
+
};
|
|
22
|
+
if (statusCode === 503) return {
|
|
23
|
+
category: "service-unavailable",
|
|
24
|
+
retryable: true
|
|
25
|
+
};
|
|
26
|
+
if (statusCode >= 400 && statusCode < 500) return {
|
|
27
|
+
category: "validation",
|
|
28
|
+
retryable: false
|
|
29
|
+
};
|
|
30
|
+
if (statusCode >= 500 && statusCode < 600) return {
|
|
31
|
+
category: "server-error",
|
|
32
|
+
retryable: true
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
category: "unknown",
|
|
36
|
+
retryable: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Classifies an arbitrary delivery failure.
|
|
41
|
+
*
|
|
42
|
+
* @param error The error to classify.
|
|
43
|
+
* @returns The receipt error category, retryability, and default code.
|
|
44
|
+
* @since 0.5.0
|
|
45
|
+
*/
|
|
46
|
+
function classifyReceiptError(error) {
|
|
47
|
+
const message = getStringProperty(error, "message") ?? String(error);
|
|
48
|
+
const name = getStringProperty(error, "name") ?? "";
|
|
49
|
+
const text = `${name} ${message}`.toLowerCase();
|
|
50
|
+
if (text.includes("timeout") || text.includes("timed out")) return {
|
|
51
|
+
category: "timeout",
|
|
52
|
+
retryable: true,
|
|
53
|
+
code: "timeout"
|
|
54
|
+
};
|
|
55
|
+
if (text.includes("network") || text.includes("connect") || text.includes("fetch failed") || text.includes("dns") || text.includes("econnreset") || text.includes("econnrefused") || text.includes("enotfound") || name === "NetworkError") return {
|
|
56
|
+
category: "network",
|
|
57
|
+
retryable: true,
|
|
58
|
+
code: "network"
|
|
59
|
+
};
|
|
60
|
+
if (text.includes("authentication") || text.includes("authenticate") || text.includes("authorization") || text.includes("authorize") || /\bauth\b/.test(text) || text.includes("unauthorized") || text.includes("not authorized") || text.includes("forbidden") || text.includes("invalid api key") || text.includes("invalid token") || text.includes("401") || text.includes("403")) return {
|
|
61
|
+
category: "auth",
|
|
62
|
+
retryable: false,
|
|
63
|
+
code: "auth"
|
|
64
|
+
};
|
|
65
|
+
if (text.includes("rate limit") || text.includes("rate-limit") || text.includes("too many requests") || text.includes("quota") || text.includes("throttle") || text.includes("429")) return {
|
|
66
|
+
category: "rate-limit",
|
|
67
|
+
retryable: true,
|
|
68
|
+
code: "rate-limit"
|
|
69
|
+
};
|
|
70
|
+
if (text.includes("invalid") || text.includes("malformed") || text.includes("validation") || text.includes("bad request") || text.includes("400") || text.includes("422")) return {
|
|
71
|
+
category: "validation",
|
|
72
|
+
retryable: false,
|
|
73
|
+
code: "validation"
|
|
74
|
+
};
|
|
75
|
+
if (text.includes("rejected") || text.includes("bounce") || text.includes("complaint") || text.includes("suppressed") || text.includes("unsubscribed")) return {
|
|
76
|
+
category: "rejected",
|
|
77
|
+
retryable: false,
|
|
78
|
+
code: "rejected"
|
|
79
|
+
};
|
|
80
|
+
if (text.includes("service unavailable") || text.includes("temporarily unavailable") || text.includes("503")) return {
|
|
81
|
+
category: "service-unavailable",
|
|
82
|
+
retryable: true,
|
|
83
|
+
code: "service-unavailable"
|
|
84
|
+
};
|
|
85
|
+
if (text.includes("internal server error") || text.includes("bad gateway") || text.includes("500") || text.includes("502")) return {
|
|
86
|
+
category: "server-error",
|
|
87
|
+
retryable: true,
|
|
88
|
+
code: "server-error"
|
|
89
|
+
};
|
|
90
|
+
if (text.includes("config") || text.includes("unsupported")) return {
|
|
91
|
+
category: "configuration",
|
|
92
|
+
retryable: false,
|
|
93
|
+
code: "configuration"
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
category: "unknown",
|
|
97
|
+
retryable: false,
|
|
98
|
+
code: "unknown"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Creates structured information about a delivery failure.
|
|
103
|
+
*
|
|
104
|
+
* @param message Human-readable error message.
|
|
105
|
+
* @param options Optional structured error metadata.
|
|
106
|
+
* @returns A structured receipt error.
|
|
107
|
+
* @since 0.5.0
|
|
108
|
+
*/
|
|
109
|
+
function createReceiptError(message, options = {}) {
|
|
110
|
+
const classification = options.statusCode == null ? classifyReceiptError(message) : classifyHttpStatus(options.statusCode);
|
|
111
|
+
const category = options.category ?? classification.category;
|
|
112
|
+
const retryable = options.retryable ?? classification.retryable;
|
|
113
|
+
const code = options.code ?? (options.statusCode == null ? options.category != null ? category : classification.code ?? category : `http.${options.statusCode}`);
|
|
114
|
+
return omitUndefined({
|
|
115
|
+
message,
|
|
116
|
+
category,
|
|
117
|
+
code,
|
|
118
|
+
retryable,
|
|
119
|
+
provider: options.provider,
|
|
120
|
+
statusCode: options.statusCode,
|
|
121
|
+
retryAfterMilliseconds: options.retryAfterMilliseconds,
|
|
122
|
+
providerDetails: options.providerDetails
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Creates a failed receipt with human-readable and structured errors.
|
|
127
|
+
*
|
|
128
|
+
* @param error Error message, structured error, or list of structured errors.
|
|
129
|
+
* @param options Optional receipt and error metadata.
|
|
130
|
+
* @returns A failed receipt.
|
|
131
|
+
* @since 0.5.0
|
|
132
|
+
*/
|
|
133
|
+
function createFailedReceipt(error, options = {}) {
|
|
134
|
+
const errors = options.errors ?? (typeof error === "string" ? [createReceiptError(error, options)] : Array.isArray(error) ? error.map((item) => typeof item === "string" ? createReceiptError(item, options) : item) : [error]);
|
|
135
|
+
const errorMessages = typeof error === "string" ? [error] : Array.isArray(error) ? error.map((item) => typeof item === "string" ? item : item.message) : errors.map((receiptError) => receiptError.message);
|
|
136
|
+
const retryable = options.retryable ?? errors.some((receiptError) => receiptError.retryable);
|
|
137
|
+
const provider = options.provider ?? errors[0]?.provider;
|
|
138
|
+
return omitUndefined({
|
|
139
|
+
successful: false,
|
|
140
|
+
errorMessages,
|
|
141
|
+
errors,
|
|
142
|
+
retryable,
|
|
143
|
+
provider,
|
|
144
|
+
attempts: options.attempts,
|
|
145
|
+
timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Parses an HTTP `Retry-After` header value into milliseconds.
|
|
150
|
+
*
|
|
151
|
+
* @param value Header value to parse.
|
|
152
|
+
* @param now Current time used when parsing HTTP dates.
|
|
153
|
+
* @returns Retry delay in milliseconds, or `undefined` if invalid.
|
|
154
|
+
* @since 0.5.0
|
|
155
|
+
*/
|
|
156
|
+
function parseRetryAfter(value, now = /* @__PURE__ */ new Date()) {
|
|
157
|
+
if (value == null || value.trim() === "") return void 0;
|
|
158
|
+
const trimmed = value.trim();
|
|
159
|
+
if (/^\d+$/.test(trimmed)) {
|
|
160
|
+
const delay$1 = Number(trimmed) * 1e3;
|
|
161
|
+
return delay$1;
|
|
162
|
+
}
|
|
163
|
+
const timestamp = Date.parse(trimmed);
|
|
164
|
+
if (Number.isNaN(timestamp)) return void 0;
|
|
165
|
+
const delay = timestamp - now.getTime();
|
|
166
|
+
return delay > 0 ? delay : void 0;
|
|
167
|
+
}
|
|
168
|
+
function getStringProperty(value, property) {
|
|
169
|
+
if (value instanceof Error) return value[property];
|
|
170
|
+
if (typeof value !== "object" || value == null || !(property in value)) return void 0;
|
|
171
|
+
const propertyValue = value[property];
|
|
172
|
+
return propertyValue == null ? void 0 : String(propertyValue);
|
|
173
|
+
}
|
|
174
|
+
function omitUndefined(value) {
|
|
175
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
export { classifyHttpStatus, classifyReceiptError, createFailedReceipt, createReceiptError, parseRetryAfter };
|
package/dist/transport.d.cts
CHANGED
|
@@ -6,7 +6,13 @@ import { Receipt } from "./receipt.cjs";
|
|
|
6
6
|
/**
|
|
7
7
|
* A common interface for email sending services.
|
|
8
8
|
*/
|
|
9
|
-
interface Transport {
|
|
9
|
+
interface Transport<TProviderId extends string = string> {
|
|
10
|
+
/**
|
|
11
|
+
* Stable provider identifier for receipts produced by this transport.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.5.0
|
|
14
|
+
*/
|
|
15
|
+
readonly id: TProviderId;
|
|
10
16
|
/**
|
|
11
17
|
* Sends a single message using the email service.
|
|
12
18
|
* @param message The message to send.
|
|
@@ -14,21 +20,21 @@ interface Transport {
|
|
|
14
20
|
* @returns A promise that resolves to a receipt containing the result of
|
|
15
21
|
* the send operation.
|
|
16
22
|
*/
|
|
17
|
-
send(message: Message, options?: TransportOptions): Promise<Receipt
|
|
23
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId>>;
|
|
18
24
|
/**
|
|
19
25
|
* Sends multiple messages using the email service.
|
|
20
26
|
* @param messages An iterable of messages to send.
|
|
21
27
|
* @param options Optional parameters for sending the messages.
|
|
22
28
|
* @return An async iterable that yields receipts for each sent message.
|
|
23
29
|
*/
|
|
24
|
-
sendMany(messages: Iterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
30
|
+
sendMany(messages: Iterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
|
|
25
31
|
/**
|
|
26
32
|
* Sends multiple messages using the email service.
|
|
27
33
|
* @param messages An async iterable of messages to send.
|
|
28
34
|
* @param options Optional parameters for sending the messages.
|
|
29
35
|
* @return An async iterable that yields receipts for each sent message.
|
|
30
36
|
*/
|
|
31
|
-
sendMany(messages: AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
37
|
+
sendMany(messages: AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* Options for sending messages with the email service.
|
package/dist/transport.d.ts
CHANGED
|
@@ -6,7 +6,13 @@ import { Receipt } from "./receipt.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* A common interface for email sending services.
|
|
8
8
|
*/
|
|
9
|
-
interface Transport {
|
|
9
|
+
interface Transport<TProviderId extends string = string> {
|
|
10
|
+
/**
|
|
11
|
+
* Stable provider identifier for receipts produced by this transport.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.5.0
|
|
14
|
+
*/
|
|
15
|
+
readonly id: TProviderId;
|
|
10
16
|
/**
|
|
11
17
|
* Sends a single message using the email service.
|
|
12
18
|
* @param message The message to send.
|
|
@@ -14,21 +20,21 @@ interface Transport {
|
|
|
14
20
|
* @returns A promise that resolves to a receipt containing the result of
|
|
15
21
|
* the send operation.
|
|
16
22
|
*/
|
|
17
|
-
send(message: Message, options?: TransportOptions): Promise<Receipt
|
|
23
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId>>;
|
|
18
24
|
/**
|
|
19
25
|
* Sends multiple messages using the email service.
|
|
20
26
|
* @param messages An iterable of messages to send.
|
|
21
27
|
* @param options Optional parameters for sending the messages.
|
|
22
28
|
* @return An async iterable that yields receipts for each sent message.
|
|
23
29
|
*/
|
|
24
|
-
sendMany(messages: Iterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
30
|
+
sendMany(messages: Iterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
|
|
25
31
|
/**
|
|
26
32
|
* Sends multiple messages using the email service.
|
|
27
33
|
* @param messages An async iterable of messages to send.
|
|
28
34
|
* @param options Optional parameters for sending the messages.
|
|
29
35
|
* @return An async iterable that yields receipts for each sent message.
|
|
30
36
|
*/
|
|
31
|
-
sendMany(messages: AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt
|
|
37
|
+
sendMany(messages: AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* Options for sending messages with the email service.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/core",
|
|
3
|
-
"version": "0.5.0
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Simple email sending library for Node.js, Deno, Bun, and edge functions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -49,6 +49,14 @@
|
|
|
49
49
|
"import": "./dist/index.js",
|
|
50
50
|
"require": "./dist/index.cjs"
|
|
51
51
|
},
|
|
52
|
+
"./abort-signal": {
|
|
53
|
+
"types": {
|
|
54
|
+
"import": "./dist/abort-signal.d.ts",
|
|
55
|
+
"require": "./dist/abort-signal.d.cts"
|
|
56
|
+
},
|
|
57
|
+
"import": "./dist/abort-signal.js",
|
|
58
|
+
"require": "./dist/abort-signal.cjs"
|
|
59
|
+
},
|
|
52
60
|
"./address": {
|
|
53
61
|
"types": {
|
|
54
62
|
"import": "./dist/address.d.ts",
|
|
@@ -105,11 +113,6 @@
|
|
|
105
113
|
"typescript": "5.8.3"
|
|
106
114
|
},
|
|
107
115
|
"scripts": {
|
|
108
|
-
"
|
|
109
|
-
"prepublish": "tsdown",
|
|
110
|
-
"test": "tsdown && node --experimental-transform-types --test",
|
|
111
|
-
"test:bun": "tsdown && bun test",
|
|
112
|
-
"test:deno": "deno test",
|
|
113
|
-
"test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test"
|
|
116
|
+
"prepublish": "mise run --no-deps :build"
|
|
114
117
|
}
|
|
115
118
|
}
|