Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,26 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@t3-oss/env-core": "0.13.8",
"axios": "1.13.2",
"csv-parse": "6.1.0",
"winston": "3.18.3",
"zod": "4.1.11"
"@t3-oss/env-core": "0.13.11",
"axios": "1.13.6",
"csv-parse": "6.2.1",
"winston": "3.19.0",
"zod": "4.3.6"
},
"devDependencies": {
"@eslint/js": "9.39.1",
"@jest/globals": "30.2.0",
"@swc/core": "1.15.2",
"@eslint/js": "10.0.1",
"@jest/globals": "30.3.0",
"@swc/core": "1.15.21",
"@swc/jest": "0.2.39",
"@types/jest": "30.0.0",
"@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"eslint": "9.39.1",
"@types/node": "25.5.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"eslint": "10.1.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"jest": "30.2.0",
"prettier": "3.6.2",
"typescript": "5.9.3"
"jest": "30.3.0",
"prettier": "3.8.1",
"typescript": "6.0.2"
},
"sideEffects": false
}
1,440 changes: 689 additions & 751 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

135 changes: 49 additions & 86 deletions src/client/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,13 @@ export const DEFAULT_USER_AGENT = `iterable-api/${packageJson.version}`;
*/
export class BaseIterableClient {
public client: AxiosInstance;
#httpAgent?: http.Agent;
#httpsAgent?: https.Agent;

constructor(config?: IterableConfig, injectedClient?: AxiosInstance) {
const clientConfig = config || createIterableConfig();

if (injectedClient) {
this.client = injectedClient;
} else {
// Create agents with keepAlive for better performance
this.#httpAgent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
});
this.#httpsAgent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
});

const defaultHeaders = {
"Api-Key": clientConfig.apiKey,
"Content-Type": "application/json",
Expand All @@ -64,8 +52,14 @@ export class BaseIterableClient {
...(clientConfig.customHeaders || {}),
},
timeout: clientConfig.timeout || 30000,
httpAgent: this.#httpAgent,
httpsAgent: this.#httpsAgent,
httpAgent: new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
}),
httpsAgent: new https.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
}),
maxRedirects: 5,
});
}
Expand Down Expand Up @@ -140,86 +134,55 @@ export class BaseIterableClient {
}

/**
* Parse NDJSON (newline-delimited JSON) response into an array of objects
* Clean up HTTP agents to prevent Jest from hanging
* Should be called when the client is no longer needed
*/
public parseNdjson(data: string): any[] {
if (!data) {
return [];
}

const lines = data.trim().split("\n");
const results: any[] = [];

for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine) {
try {
const parsed = JSON.parse(trimmedLine);
results.push(parsed);
} catch (error) {
// Skip invalid JSON lines but log them
logger.warn("Failed to parse NDJSON line", {
line: trimmedLine,
error: error instanceof Error ? error.message : String(error),
});
}
}
}

return results;
public destroy(): void {
this.client.defaults.httpAgent?.destroy();
this.client.defaults.httpsAgent?.destroy();
}
}

/**
* Parse CSV response into an array of objects using csv-parse library
* @throws IterableResponseValidationError if CSV parsing fails
*/
public parseCsv(response: AxiosResponse<string>): Record<string, string>[] {
if (!response.data) {
return [];
}

try {
return csvParse(response.data, {
columns: true, // Use first line as headers
skip_empty_lines: true,
trim: true,
});
} catch (error) {
// Throw validation error to maintain consistent error handling
// This allows callers to handle parse failures appropriately
throw new IterableResponseValidationError(
200,
response.data,
`CSV parse error: ${error instanceof Error ? error.message : String(error)}`,
response.config?.url
);
}
/**
* @throws IterableResponseValidationError if CSV parsing fails
*/
export function parseCsv(
response: AxiosResponse<string>
): Record<string, string>[] {
if (!response.data) {
return [];
}

public validateResponse<T>(
response: { data: unknown; config?: { url?: string } },
schema: z.ZodSchema<T>
): T {
const result = schema.safeParse(response.data);
if (!result.success) {
throw new IterableResponseValidationError(
200,
response.data,
result.error.message,
response.config?.url
);
}
return result.data;
try {
return csvParse(response.data, {
columns: true,
skip_empty_lines: true,
trim: true,
});
} catch (error) {
throw new IterableResponseValidationError(
200,
response.data,
`CSV parse error: ${error instanceof Error ? error.message : String(error)}`,
response.config?.url
);
}
}

/**
* Clean up HTTP agents to prevent Jest from hanging
* Should be called when the client is no longer needed
*/
public destroy(): void {
this.#httpAgent?.destroy();
this.#httpsAgent?.destroy();
export function validateResponse<T>(
response: { data: unknown; config?: { url?: string } },
schema: z.ZodSchema<T>
): T {
const result = schema.safeParse(response.data);
if (!result.success) {
throw new IterableResponseValidationError(
200,
response.data,
result.error.message,
response.config?.url
);
}
return result.data;
}

// Type helper for mixins
Expand Down
29 changes: 15 additions & 14 deletions src/client/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
IterableSuccessResponseSchema,
} from "../types/common.js";
import type { BaseIterableClient, Constructor } from "./base.js";
import { parseCsv, validateResponse } from "./base.js";

/**
* Campaigns operations mixin
Expand Down Expand Up @@ -62,7 +63,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
url,
opts?.signal ? { signal: opts.signal } : {}
);
return this.validateResponse(response, GetCampaignsResponseSchema);
return validateResponse(response, GetCampaignsResponseSchema);
}

async getCampaign(
Expand All @@ -73,7 +74,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
`/api/campaigns/${params.id}`,
opts?.signal ? { signal: opts.signal } : {}
);
return this.validateResponse(response, GetCampaignResponseSchema);
return validateResponse(response, GetCampaignResponseSchema);
}

async createBlastCampaign(
Expand All @@ -83,14 +84,14 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
...params,
scheduleSend: false,
});
return this.validateResponse(response, CreateCampaignResponseSchema);
return validateResponse(response, CreateCampaignResponseSchema);
}

async createTriggeredCampaign(
params: CreateTriggeredCampaignParams
): Promise<CreateCampaignResponse> {
const response = await this.client.post("/api/campaigns/create", params);
return this.validateResponse(response, CreateCampaignResponseSchema);
return validateResponse(response, CreateCampaignResponseSchema);
}

async getChildCampaigns(
Expand Down Expand Up @@ -118,7 +119,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
url,
opts?.signal ? { signal: opts.signal } : {}
);
return this.validateResponse(response, GetChildCampaignsResponseSchema);
return validateResponse(response, GetChildCampaignsResponseSchema);
}

async getCampaignMetrics(
Expand All @@ -139,7 +140,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
);

// Parse CSV response into array of objects
return this.parseCsv(response);
return parseCsv(response);
}

async scheduleCampaign(
Expand All @@ -150,7 +151,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
`/api/campaigns/${campaignId}/schedule`,
scheduleParams
);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}

/**
Expand All @@ -161,7 +162,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
params: AbortCampaignParams
): Promise<IterableSuccessResponse> {
const response = await this.client.post("/api/campaigns/abort", params);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}

/**
Expand All @@ -171,7 +172,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
params: CancelCampaignParams
): Promise<IterableSuccessResponse> {
const response = await this.client.post("/api/campaigns/cancel", params);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}

/**
Expand All @@ -185,7 +186,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
"/api/campaigns/activateTriggered",
params
);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}

/**
Expand All @@ -199,7 +200,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
"/api/campaigns/deactivateTriggered",
params
);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}

/**
Expand All @@ -214,7 +215,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
params: ArchiveCampaignsParams
): Promise<ArchiveCampaignsResponse> {
const response = await this.client.post("/api/campaigns/archive", params);
return this.validateResponse(response, ArchiveCampaignsResponseSchema);
return validateResponse(response, ArchiveCampaignsResponseSchema);
}

/**
Expand All @@ -224,7 +225,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
params: TriggerCampaignParams
): Promise<IterableSuccessResponse> {
const response = await this.client.post("/api/campaigns/trigger", params);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}

/**
Expand All @@ -236,7 +237,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
const response = await this.client.post(
`/api/campaigns/${params.campaignId}/send`
);
return this.validateResponse(response, IterableSuccessResponseSchema);
return validateResponse(response, IterableSuccessResponseSchema);
}
};
}
Loading
Loading