@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 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 | $15/mo | 15,000 | Unlimited (via your key) | 200 req/min |
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 client;
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.client = axios_1.default.create({
15
- baseURL: config.baseUrl || 'https://api.notifykit.dev',
16
- headers: {
17
- 'X-API-Key': config.apiKey,
18
- 'Content-Type': 'application/json',
19
- },
20
- });
21
- this.client.interceptors.response.use((response) => {
22
- const apiResponse = response.data;
23
- if (apiResponse && apiResponse.success === false) {
24
- throw new errors_1.NotifyKitError(apiResponse.error || apiResponse.message || 'Request failed', response.status, apiResponse);
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
- return apiResponse?.data ?? apiResponse;
27
- }, (error) => {
28
- if (error.response) {
29
- const data = error.response.data;
30
- const statusCode = error.response.status;
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
- else if (data?.message) {
43
- if (Array.isArray(data.message)) {
44
- errors = data.message;
45
- message = 'Validation failed';
46
- }
47
- else {
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
- throw new errors_1.NotifyKitError(error.message || 'Network error occurred');
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 await this.client.get('/api/v1/ping');
107
+ return this.request('GET', '/api/v1/ping');
63
108
  }
64
109
  /** Get API information */
65
110
  async getApiInfo() {
66
- return await this.client.get('/api/v1/info');
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 await this.client.post('/api/v1/notifications/email', options);
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 await this.client.post('/api/v1/notifications/webhook', options);
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 await this.client.get(`/api/v1/notifications/jobs/${jobId}`);
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
- return await this.client.get('/api/v1/notifications/jobs', {
86
- params: options,
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 await this.client.post(`/api/v1/notifications/jobs/${jobId}/retry`);
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": "1.3.0",
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": ">=14.0.0"
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"