@notifykit/sdk 1.3.0 → 2.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.
- package/README.md +13 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +103 -52
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -322,6 +322,18 @@ try {
|
|
|
322
322
|
}
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
+
### Automatic Retries & Timeouts
|
|
326
|
+
|
|
327
|
+
The SDK retries transient failures automatically:
|
|
328
|
+
|
|
329
|
+
- Retries up to **2 times** (3 attempts total) on **5xx**, **429**, and network errors, with backoff (~200ms → ~500ms).
|
|
330
|
+
- On **429**, it waits for the server's `retryAfter` (capped at 10s) before retrying.
|
|
331
|
+
- **Other 4xx errors** (e.g. 400, 401, 409) fail immediately — they are never retried.
|
|
332
|
+
- Non-idempotent requests (`sendEmail`, `sendWebhook`, `retryJob`) are **not** retried on a timeout or dropped connection, to avoid duplicate sends.
|
|
333
|
+
- Every request times out after **10 seconds**.
|
|
334
|
+
|
|
335
|
+
> This is distinct from NotifyKit's server-side **webhook delivery** retries (see [Sending Webhooks](#sending-webhooks)), which control how delivery to your endpoint is re-attempted.
|
|
336
|
+
|
|
325
337
|
---
|
|
326
338
|
|
|
327
339
|
## API Reference
|
|
@@ -370,7 +382,7 @@ import type {
|
|
|
370
382
|
| ------- | ------- | -------------- | -------------------------- | ------------ |
|
|
371
383
|
| Free | $0 | 100 (shared) | 100 (shared with webhooks) | 5 req/min |
|
|
372
384
|
| Indie | $5/mo | 4,000 | Unlimited (via your key) | 50 req/min |
|
|
373
|
-
| Startup | $
|
|
385
|
+
| Startup | $10/mo | 15,000 | Unlimited (via your key) | 200 req/min |
|
|
374
386
|
|
|
375
387
|
---
|
|
376
388
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { NotifyKitConfig, SendEmailOptions, SendWebhookOptions, JobResponse, JobStatus, JobSummary, ApiInfo, PaginationMeta, RetryJobResponse } from './types';
|
|
2
2
|
export declare class NotifyKitClient {
|
|
3
|
-
private
|
|
3
|
+
private readonly baseUrl;
|
|
4
|
+
private readonly headers;
|
|
4
5
|
constructor(config: NotifyKitConfig);
|
|
6
|
+
private request;
|
|
5
7
|
/** Test API connection */
|
|
6
8
|
ping(): Promise<string>;
|
|
7
9
|
/** Get API information */
|
package/dist/client.js
CHANGED
|
@@ -1,94 +1,145 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.NotifyKitClient = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
4
|
const errors_1 = require("./errors");
|
|
5
|
+
/** Abort a request that hasn't responded within this window. */
|
|
6
|
+
const TIMEOUT_MS = 10000;
|
|
7
|
+
/** Backoff delays between retries; length also caps the retry count (2 retries / 3 attempts). */
|
|
8
|
+
const RETRY_DELAYS_MS = [200, 500];
|
|
9
|
+
/** Upper bound for a server-provided Retry-After so a caller can't be stalled indefinitely. */
|
|
10
|
+
const MAX_RETRY_AFTER_MS = 10000;
|
|
11
|
+
/**
|
|
12
|
+
* Methods we can safely re-send after a transport failure (timeout / dropped connection).
|
|
13
|
+
* A timed-out POST may already have been processed by the server, so retrying it could
|
|
14
|
+
* duplicate a notification — only idempotent methods are retried on transport errors.
|
|
15
|
+
*/
|
|
16
|
+
const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD']);
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
/** Transient server states worth retrying regardless of HTTP method. */
|
|
21
|
+
function isRetryableStatus(status) {
|
|
22
|
+
return status >= 500 || status === 429;
|
|
23
|
+
}
|
|
24
|
+
function describeTransportError(err) {
|
|
25
|
+
if (err instanceof Error && err.name === 'TimeoutError')
|
|
26
|
+
return 'Request timed out';
|
|
27
|
+
if (err instanceof Error)
|
|
28
|
+
return err.message;
|
|
29
|
+
return 'Network error occurred';
|
|
30
|
+
}
|
|
31
|
+
/** Build a NotifyKitError from a non-OK (or success:false) response body. */
|
|
32
|
+
function toError(status, statusText, data) {
|
|
33
|
+
let message = statusText || 'Request failed';
|
|
34
|
+
let errors;
|
|
35
|
+
let retryAfter;
|
|
36
|
+
if (data) {
|
|
37
|
+
const detail = data.error ?? data.message;
|
|
38
|
+
if (Array.isArray(detail)) {
|
|
39
|
+
errors = detail;
|
|
40
|
+
message = data.error ? detail.join(', ') : 'Validation failed';
|
|
41
|
+
}
|
|
42
|
+
else if (detail != null) {
|
|
43
|
+
message = String(detail);
|
|
44
|
+
}
|
|
45
|
+
if (status === 429 && typeof data.retryAfter === 'number') {
|
|
46
|
+
retryAfter = data.retryAfter;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return new errors_1.NotifyKitError(message, status, data, errors, retryAfter);
|
|
50
|
+
}
|
|
9
51
|
class NotifyKitClient {
|
|
10
52
|
constructor(config) {
|
|
11
53
|
if (!config.apiKey) {
|
|
12
54
|
throw new Error('API key is required');
|
|
13
55
|
}
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
this.baseUrl = (config.baseUrl ?? 'https://api.notifykit.dev').replace(/\/$/, '');
|
|
57
|
+
this.headers = {
|
|
58
|
+
'X-API-Key': config.apiKey,
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async request(method, path, body) {
|
|
63
|
+
const retryTransport = IDEMPOTENT_METHODS.has(method);
|
|
64
|
+
for (let attempt = 0;; attempt++) {
|
|
65
|
+
const isLastAttempt = attempt >= RETRY_DELAYS_MS.length;
|
|
66
|
+
// --- Send ---
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
70
|
+
method,
|
|
71
|
+
headers: this.headers,
|
|
72
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
73
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
74
|
+
});
|
|
25
75
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let message = error.message;
|
|
32
|
-
let errors = null;
|
|
33
|
-
if (data?.error) {
|
|
34
|
-
if (Array.isArray(data.error)) {
|
|
35
|
-
errors = data.error;
|
|
36
|
-
message = data.error.join(', ');
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
message = data.error;
|
|
40
|
-
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// Transport failure: retry only idempotent requests, and only while attempts remain.
|
|
78
|
+
if (retryTransport && !isLastAttempt) {
|
|
79
|
+
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
80
|
+
continue;
|
|
41
81
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
message = data.message;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const retryAfter = statusCode === 429 && typeof data?.retryAfter === 'number' ? data.retryAfter : undefined;
|
|
52
|
-
throw new errors_1.NotifyKitError(message, statusCode, data, errors, retryAfter);
|
|
82
|
+
throw new errors_1.NotifyKitError(describeTransportError(err));
|
|
83
|
+
}
|
|
84
|
+
const data = await res.json().catch(() => null);
|
|
85
|
+
// --- Success ---
|
|
86
|
+
if (res.ok && data?.success !== false) {
|
|
87
|
+
return (data?.data ?? data);
|
|
53
88
|
}
|
|
54
|
-
|
|
55
|
-
|
|
89
|
+
// --- Failure ---
|
|
90
|
+
const error = toError(res.status, res.statusText, data);
|
|
91
|
+
// Retry transient server states (5xx / 429) for any method; a 4xx fails immediately.
|
|
92
|
+
if (isRetryableStatus(res.status) && !isLastAttempt) {
|
|
93
|
+
const delay = error.retryAfter != null
|
|
94
|
+
? Math.min(error.retryAfter * 1000, MAX_RETRY_AFTER_MS)
|
|
95
|
+
: RETRY_DELAYS_MS[attempt];
|
|
96
|
+
await sleep(delay);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
56
101
|
}
|
|
57
102
|
// ================================
|
|
58
103
|
// APP
|
|
59
104
|
// ================================
|
|
60
105
|
/** Test API connection */
|
|
61
106
|
async ping() {
|
|
62
|
-
return
|
|
107
|
+
return this.request('GET', '/api/v1/ping');
|
|
63
108
|
}
|
|
64
109
|
/** Get API information */
|
|
65
110
|
async getApiInfo() {
|
|
66
|
-
return
|
|
111
|
+
return this.request('GET', '/api/v1/info');
|
|
67
112
|
}
|
|
68
113
|
// ================================
|
|
69
114
|
// NOTIFICATIONS
|
|
70
115
|
// ================================
|
|
71
116
|
/** Send an email notification */
|
|
72
117
|
async sendEmail(options) {
|
|
73
|
-
return
|
|
118
|
+
return this.request('POST', '/api/v1/notifications/email', options);
|
|
74
119
|
}
|
|
75
120
|
/** Send a webhook notification */
|
|
76
121
|
async sendWebhook(options) {
|
|
77
|
-
return
|
|
122
|
+
return this.request('POST', '/api/v1/notifications/webhook', options);
|
|
78
123
|
}
|
|
79
124
|
/** Get job status by ID */
|
|
80
125
|
async getJob(jobId) {
|
|
81
|
-
return
|
|
126
|
+
return this.request('GET', `/api/v1/notifications/jobs/${jobId}`);
|
|
82
127
|
}
|
|
83
128
|
/** List jobs with optional filters */
|
|
84
129
|
async listJobs(options) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
let path = '/api/v1/notifications/jobs';
|
|
131
|
+
if (options) {
|
|
132
|
+
const params = new URLSearchParams(Object.entries(options)
|
|
133
|
+
.filter(([, v]) => v !== undefined)
|
|
134
|
+
.map(([k, v]) => [k, String(v)]));
|
|
135
|
+
if (params.size > 0)
|
|
136
|
+
path += `?${params}`;
|
|
137
|
+
}
|
|
138
|
+
return this.request('GET', path);
|
|
88
139
|
}
|
|
89
140
|
/** Retry a failed job */
|
|
90
141
|
async retryJob(jobId) {
|
|
91
|
-
return
|
|
142
|
+
return this.request('POST', `/api/v1/notifications/jobs/${jobId}/retry`);
|
|
92
143
|
}
|
|
93
144
|
}
|
|
94
145
|
exports.NotifyKitClient = NotifyKitClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notifykit/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Official NotifyKit SDK for Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"prepublishOnly": "npm run build"
|
|
15
15
|
},
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"notifykit",
|
|
@@ -33,9 +33,7 @@
|
|
|
33
33
|
"url": "https://github.com/brayzonn/notifykit-sdk/issues"
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://github.com/brayzonn/notifykit-sdk#readme",
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"axios": "^1.6.0"
|
|
38
|
-
},
|
|
36
|
+
"dependencies": {},
|
|
39
37
|
"devDependencies": {
|
|
40
38
|
"@types/node": "^20.0.0",
|
|
41
39
|
"typescript": "^5.0.0"
|