Skip to content

Commit 2480e74

Browse files
Yang JingYang Jing
authored andcommitted
Merge remote-tracking branch 'origin/main'
2 parents bd2455f + e542c58 commit 2480e74

2 files changed

Lines changed: 33 additions & 75 deletions

File tree

src/client.ts

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -437,61 +437,9 @@ export class HippoDid {
437437
headers["X-Tenant-Id"] = this.tenantId;
438438
}
439439

440-
let lastError: Error | undefined;
441-
442-
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
443-
if (attempt > 0) {
444-
const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
445-
const jitter = Math.random() * backoff * 0.3;
446-
await sleep(backoff + jitter);
447-
}
448-
449-
let response: Response;
450-
try {
451-
response = await fetch(url, { method: "POST", headers, body: form });
452-
} catch (err) {
453-
lastError = err instanceof Error ? err : new Error(String(err));
454-
continue;
455-
}
456-
457-
if (response.ok) {
458-
const text = await response.text();
459-
return JSON.parse(text) as T;
460-
}
461-
462-
let errorMessage: string;
463-
try {
464-
const errBody = await response.json();
465-
errorMessage = errBody.message ?? errBody.error ?? JSON.stringify(errBody);
466-
} catch {
467-
errorMessage = `HTTP ${response.status}`;
468-
}
469-
470-
if (response.status === 429 || response.status >= 500) {
471-
if (response.status === 429 && attempt === this.maxRetries) {
472-
const retryAfter = response.headers.get("Retry-After");
473-
throw new RateLimitError(
474-
errorMessage,
475-
retryAfter ? parseInt(retryAfter, 10) * 1000 : 0,
476-
);
477-
}
478-
lastError = new HippoDidError(errorMessage, response.status);
479-
continue;
480-
}
481-
482-
if (response.status === 401 || response.status === 403) {
483-
throw new AuthenticationError(errorMessage);
484-
}
485-
if (response.status === 404) {
486-
throw new NotFoundError(errorMessage);
487-
}
488-
if (response.status === 400 || response.status === 422) {
489-
throw new ValidationError(errorMessage, response.status);
490-
}
491-
throw new HippoDidError(errorMessage, response.status);
492-
}
493-
494-
throw lastError ?? new HippoDidError("Request failed after retries");
440+
return this.executeWithRetry(
441+
() => fetch(url, { method: "POST", headers, body: form }),
442+
);
495443
}
496444

497445
private async request<T>(
@@ -503,15 +451,30 @@ export class HippoDid {
503451
const url = `${this.baseUrl}${path}`;
504452
const headers: Record<string, string> = {
505453
Authorization: `Bearer ${this.apiKey}`,
506-
"Content-Type": "application/json",
507454
Accept: "application/json",
508455
...extraHeaders,
509456
};
510-
457+
if (body != null) {
458+
headers["Content-Type"] = "application/json";
459+
}
511460
if (this.tenantId) {
512461
headers["X-Tenant-Id"] = this.tenantId;
513462
}
514463

464+
return this.executeWithRetry(
465+
() =>
466+
fetch(url, {
467+
method,
468+
headers,
469+
body: body != null ? JSON.stringify(body) : undefined,
470+
}),
471+
);
472+
}
473+
474+
/** Shared retry/backoff/error-dispatch loop used by request() and requestMultipart(). */
475+
private async executeWithRetry<T>(
476+
doFetch: () => Promise<Response>,
477+
): Promise<T> {
515478
let lastError: Error | undefined;
516479

517480
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
@@ -523,11 +486,7 @@ export class HippoDid {
523486

524487
let response: Response;
525488
try {
526-
response = await fetch(url, {
527-
method,
528-
headers,
529-
body: body != null ? JSON.stringify(body) : undefined,
530-
});
489+
response = await doFetch();
531490
} catch (err) {
532491
lastError = err instanceof Error ? err : new Error(String(err));
533492
continue;
@@ -540,7 +499,6 @@ export class HippoDid {
540499
return JSON.parse(text) as T;
541500
}
542501

543-
// Parse error body
544502
let errorMessage: string;
545503
try {
546504
const errBody = await response.json();
@@ -550,7 +508,6 @@ export class HippoDid {
550508
errorMessage = `HTTP ${response.status}`;
551509
}
552510

553-
// Retryable: 429 or 5xx
554511
if (response.status === 429 || response.status >= 500) {
555512
if (response.status === 429 && attempt === this.maxRetries) {
556513
const retryAfter = response.headers.get("Retry-After");
@@ -563,7 +520,6 @@ export class HippoDid {
563520
continue;
564521
}
565522

566-
// Non-retryable errors
567523
if (response.status === 401 || response.status === 403) {
568524
throw new AuthenticationError(errorMessage);
569525
}
@@ -586,11 +542,11 @@ function normaliseBatchJob(raw: BatchJobApiResponse): BatchJob {
586542
jobId: raw.jobId,
587543
status: raw.status,
588544
dryRun: raw.dryRun,
589-
totalRows: raw.progress.totalRows,
590-
succeeded: raw.progress.succeeded,
591-
failed: raw.progress.failed,
592-
skipped: raw.progress.skipped,
593-
errors: raw.errors.map((e) => ({
545+
totalRows: raw.progress?.totalRows ?? 0,
546+
succeeded: raw.progress?.succeeded ?? 0,
547+
failed: raw.progress?.failed ?? 0,
548+
skipped: raw.progress?.skipped ?? 0,
549+
errors: (raw.errors ?? []).map((e) => ({
594550
rowIndex: e.rowIndex,
595551
externalId: e.externalId,
596552
message: e.message,
@@ -604,7 +560,7 @@ function normaliseBatchJob(raw: BatchJobApiResponse): BatchJob {
604560
/** Convert an array of row objects to a CSV string. */
605561
function rowsToCsv(rows: Record<string, string>[]): string {
606562
if (rows.length === 0) return "";
607-
const columns = Object.keys(rows[0]);
563+
const columns = [...new Set(rows.flatMap(Object.keys))];
608564
const escapeCsv = (v: string) => {
609565
if (v.includes(",") || v.includes('"') || v.includes("\n")) {
610566
return `"${v.replace(/"/g, '""')}"`;

src/strategies.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ function formatProfile(profile: CharacterProfile): string {
2525
const parts: string[] = [];
2626
if (profile.personality) parts.push(`Personality: ${profile.personality}`);
2727
if (profile.background) parts.push(`Background: ${profile.background}`);
28-
if (profile.rules.length > 0) {
29-
parts.push(`Rules:\n${profile.rules.map((r) => `- ${r}`).join("\n")}`);
28+
const rules = profile.rules ?? [];
29+
if (rules.length > 0) {
30+
parts.push(`Rules:\n${rules.map((r) => `- ${r}`).join("\n")}`);
3031
}
31-
if (Object.keys(profile.customFields).length > 0) {
32-
const fields = Object.entries(profile.customFields)
32+
const customFields = profile.customFields ?? {};
33+
if (Object.keys(customFields).length > 0) {
34+
const fields = Object.entries(customFields)
3335
.map(([k, v]) => `- ${k}: ${v}`)
3436
.join("\n");
3537
parts.push(`Custom Fields:\n${fields}`);

0 commit comments

Comments
 (0)