Skip to content
Draft
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
29 changes: 29 additions & 0 deletions src/lib/fli/seats-aero/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ import type { SearchRequestParams, SearchResponse } from "../models/seats-aero";
import {
SearchRequestParamsSchema,
SearchResponseSchema,
AvailabilityTripSchema,
} from "../models/seats-aero";
import { z } from "zod";

export type SeatsAeroClientConfig = {
apiKey: string;
baseUrl?: string;
fetchImpl?: typeof fetch;
/**
* Controls how strictly responses are validated.
* - "full" (default): validate entire response with deep schemas
* - "light": validate only top-level fields and AvailabilityTrips to reduce CPU
*/
validationMode?: "full" | "light";
};

/**
Expand Down Expand Up @@ -41,11 +49,13 @@ export class SeatsAeroClient {
private readonly apiKey: string;
private readonly baseUrl: string;
private readonly fetchImpl: typeof fetch;
private readonly validationMode: "full" | "light";

constructor(config: SeatsAeroClientConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl ?? "https://seats.aero/partnerapi";
this.fetchImpl = config.fetchImpl ?? fetch;
this.validationMode = config.validationMode ?? "full";
}

/**
Expand Down Expand Up @@ -79,6 +89,25 @@ export class SeatsAeroClient {
}

const data = await response.json();

if (this.validationMode === "light") {
// Validate only the fields we actually consume downstream to reduce CPU cost
const LightAvailabilitySchema = z
.object({
AvailabilityTrips: z.array(AvailabilityTripSchema).nullable(),
})
.passthrough();

const LightSearchResponseSchema = z.object({
data: z.array(LightAvailabilitySchema),
count: z.number(),
hasMore: z.boolean(),
cursor: z.number(),
});

return LightSearchResponseSchema.parse(data) as SearchResponse;
}

return SearchResponseSchema.parse(data);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface SeatsAeroClientLike {
take: number;
only_direct_flights: boolean;
include_filtered: boolean;
minify_trips?: boolean;
cursor?: number;
}) => Promise<SeatsAeroSearchResponse>;
}
Expand Down Expand Up @@ -73,6 +74,8 @@ export async function paginateSeatsAeroSearch({
delay: "30 seconds" as const,
backoff: "constant" as const,
},
// Reduce risk of step-level timeouts; API responses are fast,
// but JSON parsing + DB upserts can be heavy. Keep ample time.
timeout: "10 minutes" as const,
};

Expand All @@ -90,9 +93,12 @@ export async function paginateSeatsAeroSearch({
start_date: params.searchStartDate,
end_date: params.searchEndDate,
include_trips: true,
take: 1000,
// Use smaller page size to cut payload and parsing time
take: 500,
only_direct_flights: false,
include_filtered: false,
// Ask API to reduce per-trip fields when supported
minify_trips: true,
cursor: currentCursor,
});

Expand Down
2 changes: 2 additions & 0 deletions src/workers/workflows/process-seats-aero-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class ProcessSeatsAeroSearchWorkflowBase extends WorkflowEntrypoint<

const client = createSeatsAeroClient({
apiKey: this.env.SEATS_AERO_API_KEY,
// Use lighter response validation to avoid CPU-bound stalls
validationMode: "light",
});

const { totalProcessed } = await paginateSeatsAeroSearch({
Expand Down
Loading