@voyant-travel/flights 0.119.2

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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +116 -0
  3. package/dist/contract/adapter.d.ts +2 -0
  4. package/dist/contract/adapter.d.ts.map +1 -0
  5. package/dist/contract/adapter.js +1 -0
  6. package/dist/contract/adapter.test.d.ts +2 -0
  7. package/dist/contract/adapter.test.d.ts.map +1 -0
  8. package/dist/contract/adapter.test.js +171 -0
  9. package/dist/contract/post-book-types.d.ts +2 -0
  10. package/dist/contract/post-book-types.d.ts.map +1 -0
  11. package/dist/contract/post-book-types.js +1 -0
  12. package/dist/contract/schemas.d.ts +2 -0
  13. package/dist/contract/schemas.d.ts.map +1 -0
  14. package/dist/contract/schemas.js +1 -0
  15. package/dist/contract/schemas.test.d.ts +2 -0
  16. package/dist/contract/schemas.test.d.ts.map +1 -0
  17. package/dist/contract/schemas.test.js +360 -0
  18. package/dist/contract/types.d.ts +2 -0
  19. package/dist/contract/types.d.ts.map +1 -0
  20. package/dist/contract/types.js +1 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +14 -0
  24. package/dist/orchestration/fan-out.d.ts +81 -0
  25. package/dist/orchestration/fan-out.d.ts.map +1 -0
  26. package/dist/orchestration/fan-out.js +132 -0
  27. package/dist/orchestration/fan-out.test.d.ts +2 -0
  28. package/dist/orchestration/fan-out.test.d.ts.map +1 -0
  29. package/dist/orchestration/fan-out.test.js +271 -0
  30. package/dist/orchestration/fingerprint.d.ts +20 -0
  31. package/dist/orchestration/fingerprint.d.ts.map +1 -0
  32. package/dist/orchestration/fingerprint.js +22 -0
  33. package/dist/orchestration/fingerprint.test.d.ts +2 -0
  34. package/dist/orchestration/fingerprint.test.d.ts.map +1 -0
  35. package/dist/orchestration/fingerprint.test.js +91 -0
  36. package/dist/reference/contract.d.ts +2 -0
  37. package/dist/reference/contract.d.ts.map +1 -0
  38. package/dist/reference/contract.js +1 -0
  39. package/dist/reference/local-postgres.d.ts +390 -0
  40. package/dist/reference/local-postgres.d.ts.map +1 -0
  41. package/dist/reference/local-postgres.js +194 -0
  42. package/dist/reference/static-bundle.d.ts +2 -0
  43. package/dist/reference/static-bundle.d.ts.map +1 -0
  44. package/dist/reference/static-bundle.js +1 -0
  45. package/dist/reference/static-bundle.test.d.ts +2 -0
  46. package/dist/reference/static-bundle.test.d.ts.map +1 -0
  47. package/dist/reference/static-bundle.test.js +75 -0
  48. package/dist/snapshot.d.ts +2 -0
  49. package/dist/snapshot.d.ts.map +1 -0
  50. package/dist/snapshot.js +1 -0
  51. package/dist/snapshot.test.d.ts +2 -0
  52. package/dist/snapshot.test.d.ts.map +1 -0
  53. package/dist/snapshot.test.js +96 -0
  54. package/package.json +96 -0
@@ -0,0 +1,360 @@
1
+ // agent-quality: file-size exception -- owner: flights; existing coverage file stays co-located until a dedicated split preserves behavior and tests.
2
+ import { describe, expect, it } from "vitest";
3
+ import { ancillaryRequestSchema, ancillaryResponseSchema, checkInRequestSchema, checkInResponseSchema, flightAdapterCapabilitiesSchema, flightAdapterContextSchema, flightBookRequestSchema, flightBookResponseSchema, flightCancelResponseSchema, flightGetOrderResponseSchema, flightModifyRequestSchema, flightModifyResponseSchema, flightOrdersListQuerySchema, flightOrdersListResponseSchema, flightPriceRequestSchema, flightPriceResponseSchema, flightRefundRequestSchema, flightRefundResponseSchema, flightSearchRequestSchema, flightSearchResponseSchema, flightVoidResponseSchema, moneySchema, seatMapRequestSchema, seatMapResponseSchema, seatSelectionRequestSchema, seatSelectionResponseSchema, ssrRequestSchema, ssrResponseSchema, } from "./schemas.js";
4
+ const typeChecks = [
5
+ true,
6
+ true,
7
+ true,
8
+ true,
9
+ true,
10
+ true,
11
+ true,
12
+ true,
13
+ true,
14
+ true,
15
+ true,
16
+ true,
17
+ true,
18
+ true,
19
+ true,
20
+ true,
21
+ true,
22
+ true,
23
+ true,
24
+ true,
25
+ true,
26
+ true,
27
+ true,
28
+ true,
29
+ true,
30
+ true,
31
+ true,
32
+ true,
33
+ true,
34
+ true,
35
+ true,
36
+ true,
37
+ true,
38
+ true,
39
+ true,
40
+ true,
41
+ true,
42
+ true,
43
+ true,
44
+ true,
45
+ true,
46
+ true,
47
+ true,
48
+ true,
49
+ true,
50
+ true,
51
+ true,
52
+ true,
53
+ ];
54
+ void typeChecks;
55
+ const money = { amount: "600.00", currency: "USD" };
56
+ const segment = {
57
+ segmentId: "seg_1",
58
+ carrierCode: "BA",
59
+ flightNumber: "177",
60
+ departure: { iataCode: "LHR", terminal: "5", at: "2026-10-15T11:00:00+00:00" },
61
+ arrival: { iataCode: "JFK", terminal: "8", at: "2026-10-15T14:00:00-04:00" },
62
+ aircraft: "777",
63
+ cabin: "economy",
64
+ providerData: { source: "fixture" },
65
+ };
66
+ const offer = {
67
+ offerId: "offer_1",
68
+ source: "test",
69
+ itineraries: [{ segments: [segment], duration: "PT8H" }],
70
+ fareBreakdowns: [
71
+ {
72
+ passengerType: "adult",
73
+ passengerCount: 1,
74
+ baseFare: { amount: "500.00", currency: "USD" },
75
+ taxes: { amount: "100.00", currency: "USD" },
76
+ total: money,
77
+ },
78
+ ],
79
+ totalPrice: money,
80
+ validatingCarrier: "BA",
81
+ expiresAt: "2026-10-01T11:00:00Z",
82
+ lastTicketingDate: "2026-10-02",
83
+ fareBundles: [
84
+ {
85
+ id: "standard",
86
+ label: "Standard",
87
+ tier: "standard",
88
+ priceDelta: { amount: "30.00", currency: "USD" },
89
+ inclusions: {
90
+ cabinBag: { included: true, weightKg: 8 },
91
+ seatSelection: "standard",
92
+ },
93
+ },
94
+ ],
95
+ };
96
+ const passenger = {
97
+ passengerId: "pax_1",
98
+ type: "adult",
99
+ firstName: "Ada",
100
+ lastName: "Lovelace",
101
+ dateOfBirth: "1980-01-01",
102
+ documents: [
103
+ {
104
+ type: "passport",
105
+ number: "123456789",
106
+ countryOfIssue: "GB",
107
+ expiryDate: "2030-01-01",
108
+ },
109
+ ],
110
+ };
111
+ const order = {
112
+ orderId: "order_1",
113
+ pnr: "ABC123",
114
+ status: "ticketed",
115
+ offer,
116
+ passengers: [passenger],
117
+ contact: { email: "ada@example.com" },
118
+ tickets: [{ ticketNumber: "1250000000001", passengerId: "pax_1", segmentIds: ["seg_1"] }],
119
+ totalPrice: money,
120
+ createdAt: "2026-10-01T10:00:00Z",
121
+ providerData: { locator: "ABC123" },
122
+ };
123
+ const ancillarySelection = {
124
+ baggage: [{ passengerId: "pax_1", sliceIndex: 0, optionId: "bag_20kg", quantity: 1 }],
125
+ seats: [{ passengerId: "pax_1", segmentId: "seg_1", seatNumber: "12A" }],
126
+ };
127
+ const seatMap = {
128
+ segmentId: "seg_1",
129
+ aircraft: "777",
130
+ cabin: "economy",
131
+ columnLayout: ["A", "B", "C", null, "D", "E", "F"],
132
+ rows: [
133
+ {
134
+ row: 12,
135
+ seats: [
136
+ {
137
+ seatNumber: "12A",
138
+ row: 12,
139
+ column: "A",
140
+ status: "available",
141
+ category: "standard",
142
+ window: true,
143
+ },
144
+ ],
145
+ },
146
+ ],
147
+ };
148
+ const logger = {
149
+ debug() { },
150
+ info() { },
151
+ warn() { },
152
+ error() { },
153
+ };
154
+ const roundTripCases = [
155
+ [
156
+ "flightSearchRequestSchema",
157
+ flightSearchRequestSchema,
158
+ {
159
+ slices: [{ origin: "LHR", destination: "JFK", departureDate: "2026-10-15" }],
160
+ passengers: { adults: 1 },
161
+ cabin: "economy",
162
+ },
163
+ ],
164
+ ["flightSearchResponseSchema", flightSearchResponseSchema, { offers: [offer] }],
165
+ ["flightPriceRequestSchema", flightPriceRequestSchema, { offerId: "offer_1", offer }],
166
+ ["flightPriceResponseSchema", flightPriceResponseSchema, { offer, valid: true }],
167
+ [
168
+ "flightBookRequestSchema",
169
+ flightBookRequestSchema,
170
+ {
171
+ offerId: "offer_1",
172
+ offer,
173
+ passengers: [passenger],
174
+ paymentIntent: { type: "hold" },
175
+ ancillaries: ancillarySelection,
176
+ },
177
+ ],
178
+ ["flightBookResponseSchema", flightBookResponseSchema, { order }],
179
+ ["flightGetOrderResponseSchema", flightGetOrderResponseSchema, { order }],
180
+ ["flightCancelResponseSchema", flightCancelResponseSchema, { order, refundedAmount: money }],
181
+ [
182
+ "flightOrdersListQuerySchema",
183
+ flightOrdersListQuerySchema,
184
+ { cursor: "next", limit: 20, status: ["ticketed"], search: "ABC" },
185
+ ],
186
+ [
187
+ "flightOrdersListResponseSchema",
188
+ flightOrdersListResponseSchema,
189
+ { orders: [order], pagination: { total: 1, hasMore: false } },
190
+ ],
191
+ ["ancillaryRequestSchema", ancillaryRequestSchema, { offerId: "offer_1", offer }],
192
+ [
193
+ "ancillaryResponseSchema",
194
+ ancillaryResponseSchema,
195
+ {
196
+ catalog: {
197
+ baggage: [{ id: "bag_20kg", label: "20kg bag", category: "checked", price: money }],
198
+ assistance: [{ id: "wchr", label: "Wheelchair", category: "wheelchair" }],
199
+ extras: [{ id: "priority", label: "Priority", category: "boarding", price: money }],
200
+ },
201
+ validUntil: "2026-10-01T11:00:00Z",
202
+ },
203
+ ],
204
+ ["seatMapRequestSchema", seatMapRequestSchema, { offerId: "offer_1", segmentId: "seg_1", offer }],
205
+ ["seatMapResponseSchema", seatMapResponseSchema, { seatMap }],
206
+ [
207
+ "seatSelectionRequestSchema",
208
+ seatSelectionRequestSchema,
209
+ {
210
+ orderId: "order_1",
211
+ selections: [{ passengerId: "pax_1", segmentId: "seg_1", seatNumber: "12A" }],
212
+ },
213
+ ],
214
+ [
215
+ "seatSelectionResponseSchema",
216
+ seatSelectionResponseSchema,
217
+ { order, selections: [{ passengerId: "pax_1", segmentId: "seg_1", seatNumber: "12A" }] },
218
+ ],
219
+ ["checkInRequestSchema", checkInRequestSchema, { orderId: "order_1", passengerIds: ["pax_1"] }],
220
+ [
221
+ "checkInResponseSchema",
222
+ checkInResponseSchema,
223
+ {
224
+ order,
225
+ status: "checked_in",
226
+ boardingPasses: [{ passengerId: "pax_1", segmentId: "seg_1", seatNumber: "12A" }],
227
+ },
228
+ ],
229
+ [
230
+ "flightModifyRequestSchema",
231
+ flightModifyRequestSchema,
232
+ { orderId: "order_1", ancillaries: ancillarySelection },
233
+ ],
234
+ [
235
+ "flightModifyResponseSchema",
236
+ flightModifyResponseSchema,
237
+ { order, priceDifference: { amount: "-25.00", currency: "USD" } },
238
+ ],
239
+ [
240
+ "flightRefundRequestSchema",
241
+ flightRefundRequestSchema,
242
+ { orderId: "order_1", reason: "customer_request" },
243
+ ],
244
+ ["flightRefundResponseSchema", flightRefundResponseSchema, { order, refundedAmount: money }],
245
+ [
246
+ "flightVoidResponseSchema",
247
+ flightVoidResponseSchema,
248
+ { order, voidedAt: "2026-10-01T11:00:00Z" },
249
+ ],
250
+ [
251
+ "ssrRequestSchema",
252
+ ssrRequestSchema,
253
+ { orderId: "order_1", code: "WCHR", passengerIds: ["pax_1"] },
254
+ ],
255
+ ["ssrResponseSchema", ssrResponseSchema, { order, status: "requested" }],
256
+ [
257
+ "flightAdapterContextSchema",
258
+ flightAdapterContextSchema,
259
+ { connectionId: "conn_1", logger, environment: "sandbox" },
260
+ ],
261
+ [
262
+ "flightAdapterCapabilitiesSchema",
263
+ flightAdapterCapabilitiesSchema,
264
+ { provider: "demo", declared: ["flight/holds"], maxSlicesPerSearch: 2 },
265
+ ],
266
+ ];
267
+ const invalidCases = [
268
+ ["moneySchema", moneySchema, { amount: 600, currency: "USD" }],
269
+ [
270
+ "flightSearchRequestSchema",
271
+ flightSearchRequestSchema,
272
+ { slices: [], passengers: { adults: -1 } },
273
+ ],
274
+ ["flightSearchResponseSchema", flightSearchResponseSchema, { offers: [{ totalPrice: money }] }],
275
+ ["flightPriceRequestSchema", flightPriceRequestSchema, { offerId: 123 }],
276
+ ["flightPriceResponseSchema", flightPriceResponseSchema, { offer, valid: "yes" }],
277
+ ["flightBookRequestSchema", flightBookRequestSchema, { offerId: "offer_1", passengers: [{}] }],
278
+ [
279
+ "flightBookResponseSchema",
280
+ flightBookResponseSchema,
281
+ { order: { ...order, status: "unknown" } },
282
+ ],
283
+ [
284
+ "flightGetOrderResponseSchema",
285
+ flightGetOrderResponseSchema,
286
+ { order: { ...order, totalPrice: { amount: "x", currency: "USD" } } },
287
+ ],
288
+ [
289
+ "flightCancelResponseSchema",
290
+ flightCancelResponseSchema,
291
+ { order, refundedAmount: { amount: "1.00", currency: "US" } },
292
+ ],
293
+ ["flightOrdersListQuerySchema", flightOrdersListQuerySchema, { limit: 0 }],
294
+ [
295
+ "flightOrdersListResponseSchema",
296
+ flightOrdersListResponseSchema,
297
+ { orders: [order], pagination: { total: -1, hasMore: false } },
298
+ ],
299
+ ["ancillaryRequestSchema", ancillaryRequestSchema, { offerId: 123 }],
300
+ [
301
+ "ancillaryResponseSchema",
302
+ ancillaryResponseSchema,
303
+ { catalog: { baggage: [], assistance: [] } },
304
+ ],
305
+ ["seatMapRequestSchema", seatMapRequestSchema, { offerId: "offer_1" }],
306
+ ["seatMapResponseSchema", seatMapResponseSchema, { seatMap: { ...seatMap, cabin: "sofa" } }],
307
+ [
308
+ "seatSelectionRequestSchema",
309
+ seatSelectionRequestSchema,
310
+ { orderId: "order_1", selections: [{ seatNumber: "12A" }] },
311
+ ],
312
+ [
313
+ "seatSelectionResponseSchema",
314
+ seatSelectionResponseSchema,
315
+ { order, selections: [{ passengerId: "pax_1" }] },
316
+ ],
317
+ ["checkInRequestSchema", checkInRequestSchema, { passengerIds: ["pax_1"] }],
318
+ ["checkInResponseSchema", checkInResponseSchema, { order, status: "done" }],
319
+ [
320
+ "flightModifyRequestSchema",
321
+ flightModifyRequestSchema,
322
+ { orderId: "order_1", reason: "vacation" },
323
+ ],
324
+ [
325
+ "flightModifyResponseSchema",
326
+ flightModifyResponseSchema,
327
+ { order, penalties: [{ amount: "1.00", currency: "US" }] },
328
+ ],
329
+ [
330
+ "flightRefundRequestSchema",
331
+ flightRefundRequestSchema,
332
+ { orderId: "order_1", reason: "changed_mind" },
333
+ ],
334
+ [
335
+ "flightRefundResponseSchema",
336
+ flightRefundResponseSchema,
337
+ { order, refundedAmount: { amount: "x", currency: "USD" } },
338
+ ],
339
+ ["flightVoidResponseSchema", flightVoidResponseSchema, { order }],
340
+ ["ssrRequestSchema", ssrRequestSchema, { orderId: "order_1", code: "NOPE" }],
341
+ ["ssrResponseSchema", ssrResponseSchema, { order, status: "done" }],
342
+ [
343
+ "flightAdapterCapabilitiesSchema",
344
+ flightAdapterCapabilitiesSchema,
345
+ { provider: "demo", declared: ["flight/nope"] },
346
+ ],
347
+ [
348
+ "flightAdapterContextSchema",
349
+ flightAdapterContextSchema,
350
+ { connectionId: "conn_1", logger: {} },
351
+ ],
352
+ ];
353
+ describe("flight contract schemas", () => {
354
+ it.each(roundTripCases)("parses %s fixtures without changing shape", (_name, schema, value) => {
355
+ expect(schema.parse(value)).toEqual(value);
356
+ });
357
+ it.each(invalidCases)("rejects invalid %s fixtures", (_name, schema, value) => {
358
+ expect(schema.safeParse(value).success).toBe(false);
359
+ });
360
+ });
@@ -0,0 +1,2 @@
1
+ export * from "@voyant-travel/flights-contracts/contract/types";
2
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/contract/types.ts"],"names":[],"mappings":"AAAA,cAAc,iDAAiD,CAAA"}
@@ -0,0 +1 @@
1
+ export * from "@voyant-travel/flights-contracts/contract/types";
@@ -0,0 +1,10 @@
1
+ export { type AdapterLogger, CAPABILITY_NOT_SUPPORTED, type FlightAdapterCapabilities, type FlightAdapterContext, type FlightAdapterEnvironment, type FlightBookResponse, type FlightCancelReason, type FlightCancelResponse, FlightCapabilityNotSupportedError, type FlightConnectorAdapter, type FlightGetOrderResponse, type FlightPriceRequest, type FlightPriceResponse, type FlightSearchResponse, requireCapability, } from "./contract/adapter.js";
2
+ export * from "./contract/schemas.js";
3
+ export * from "./contract/types.js";
4
+ export { type ConnectionResult, type ConnectionSearchStatus, type FanOutFlightSearchOptions, type FanOutFlightSearchResult, fanOutFlightSearch, type MergedFlightOffer, } from "./orchestration/fan-out.js";
5
+ export { itineraryFingerprint } from "./orchestration/fingerprint.js";
6
+ export { type Aircraft, type Airline, type Airport, dedupeCodes, type ReferenceDataCapabilities, type ReferenceDataProvider, } from "./reference/contract.js";
7
+ export { createLocalPostgresReferenceProvider, type LocalPostgresReferenceProviderOptions, referenceAircraft, referenceAirlines, referenceAirports, } from "./reference/local-postgres.js";
8
+ export { createStaticBundleReferenceProvider, type StaticBundleProviderOptions, type StaticBundleReferenceData, } from "./reference/static-bundle.js";
9
+ export { type BuildFlightSnapshotInputOptions, buildFlightSnapshotInput, } from "./snapshot.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAClB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,iCAAiC,EACjC,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAErE,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,WAAW,EACX,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,GAC3B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oCAAoC,EACpC,KAAK,qCAAqC,EAC1C,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,GAC/B,MAAM,8BAA8B,CAAA;AAErC,OAAO,EACL,KAAK,+BAA+B,EACpC,wBAAwB,GACzB,MAAM,eAAe,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // Flight contract types — offers, orders, segments, search, booking.
2
+ // FlightConnectorAdapter contract.
3
+ export { CAPABILITY_NOT_SUPPORTED, FlightCapabilityNotSupportedError, requireCapability, } from "./contract/adapter.js";
4
+ export * from "./contract/schemas.js";
5
+ export * from "./contract/types.js";
6
+ export { fanOutFlightSearch, } from "./orchestration/fan-out.js";
7
+ // Orchestration — fingerprinting + multi-connection fan-out.
8
+ export { itineraryFingerprint } from "./orchestration/fingerprint.js";
9
+ // ReferenceDataProvider — swappable provider for global reference data.
10
+ export { dedupeCodes, } from "./reference/contract.js";
11
+ export { createLocalPostgresReferenceProvider, referenceAircraft, referenceAirlines, referenceAirports, } from "./reference/local-postgres.js";
12
+ export { createStaticBundleReferenceProvider, } from "./reference/static-bundle.js";
13
+ // Snapshot capture for booking-time integration with the catalog plane.
14
+ export { buildFlightSnapshotInput, } from "./snapshot.js";
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Multi-connection fan-out search.
3
+ *
4
+ * Parallel `searchFlights` across all of an operator's flight connections
5
+ * with per-connection timeouts, partial-success handling, and merge by
6
+ * itinerary fingerprint. Returns a merged result set with the cheapest
7
+ * offer per itinerary as the primary rank, alternates from other
8
+ * connections beneath it, and a per-connection status map.
9
+ *
10
+ * Implements voyant-cloud's `MergedFlightOffer` shape so consumers see the
11
+ * same result format regardless of where the orchestration runs.
12
+ *
13
+ * See `docs/architecture/catalog-flights-architecture.md` §4.
14
+ */
15
+ import type { FlightAdapterContext, FlightConnectorAdapter } from "../contract/adapter.js";
16
+ import type { FlightOffer, FlightSearchRequest } from "../contract/types.js";
17
+ /**
18
+ * One source's result — the primary offer plus alternates from other
19
+ * connections selling the same flight.
20
+ */
21
+ export interface MergedFlightOffer {
22
+ itineraryFingerprint: string;
23
+ /** Cheapest offer for this itinerary across all responding connections. */
24
+ cheapest: FlightOffer;
25
+ /** Other offers for the same itinerary, sorted by total price ascending. */
26
+ alternates: FlightOffer[];
27
+ /** Connection ids that returned an offer for this itinerary. */
28
+ sourceConnectionIds: string[];
29
+ }
30
+ /**
31
+ * Per-connection status, returned alongside the merged offers so callers
32
+ * can surface "X provider timed out" without losing the rest of the results.
33
+ */
34
+ export type ConnectionSearchStatus = "ok" | "timeout" | "error" | "not_found" | "capability_missing";
35
+ export interface ConnectionResult {
36
+ connectionId: string;
37
+ status: ConnectionSearchStatus;
38
+ count: number;
39
+ latencyMs: number;
40
+ errorMessage?: string;
41
+ }
42
+ export interface FanOutFlightSearchOptions {
43
+ /** Adapters to fan out across. Each carries its own connectionId in capabilities. */
44
+ adapters: ReadonlyArray<{
45
+ connectionId: string;
46
+ adapter: FlightConnectorAdapter;
47
+ /** Optional override for the adapter context per connection. */
48
+ context?: Partial<FlightAdapterContext>;
49
+ }>;
50
+ request: FlightSearchRequest;
51
+ /**
52
+ * Per-connection timeout. Default 5000ms. One slow provider doesn't
53
+ * tank the whole search — its slot is reported as `timeout` and other
54
+ * results return on time.
55
+ */
56
+ perConnectionTimeoutMs?: number;
57
+ /**
58
+ * Optional caller-supplied limit on the merged offer count. The fan-out
59
+ * still queries every connection in full; this caps the merged result.
60
+ */
61
+ limit?: number;
62
+ }
63
+ export interface FanOutFlightSearchResult {
64
+ offers: MergedFlightOffer[];
65
+ perConnection: ConnectionResult[];
66
+ }
67
+ /**
68
+ * Fan out a flight search across an operator's connections, parallelized
69
+ * with per-connection timeout, then merge by itinerary fingerprint.
70
+ *
71
+ * Partial-success semantics: connections that time out / error / report
72
+ * capability-missing are flagged in `perConnection`; the orchestration
73
+ * still returns whatever responding connections produced.
74
+ */
75
+ export declare function fanOutFlightSearch(options: FanOutFlightSearchOptions): Promise<FanOutFlightSearchResult>;
76
+ interface ConnectionFanOutOk {
77
+ connectionId: string;
78
+ offers: FlightOffer[];
79
+ }
80
+ export type { ConnectionFanOutOk };
81
+ //# sourceMappingURL=fan-out.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.d.ts","sourceRoot":"","sources":["../../src/orchestration/fan-out.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC1F,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAG5E;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,oBAAoB,EAAE,MAAM,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,EAAE,WAAW,CAAA;IACrB,4EAA4E;IAC5E,UAAU,EAAE,WAAW,EAAE,CAAA;IACzB,gEAAgE;IAChE,mBAAmB,EAAE,MAAM,EAAE,CAAA;CAC9B;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,oBAAoB,CAAA;AAEpG,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,sBAAsB,CAAA;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,qFAAqF;IACrF,QAAQ,EAAE,aAAa,CAAC;QACtB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,sBAAsB,CAAA;QAC/B,gEAAgE;QAChE,OAAO,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAA;KACxC,CAAC,CAAA;IACF,OAAO,EAAE,mBAAmB,CAAA;IAC5B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,aAAa,EAAE,gBAAgB,EAAE,CAAA;CAClC;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAgEnC;AAED,UAAU,kBAAkB;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,WAAW,EAAE,CAAA;CACtB;AAyED,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Multi-connection fan-out search.
3
+ *
4
+ * Parallel `searchFlights` across all of an operator's flight connections
5
+ * with per-connection timeouts, partial-success handling, and merge by
6
+ * itinerary fingerprint. Returns a merged result set with the cheapest
7
+ * offer per itinerary as the primary rank, alternates from other
8
+ * connections beneath it, and a per-connection status map.
9
+ *
10
+ * Implements voyant-cloud's `MergedFlightOffer` shape so consumers see the
11
+ * same result format regardless of where the orchestration runs.
12
+ *
13
+ * See `docs/architecture/catalog-flights-architecture.md` §4.
14
+ */
15
+ import { itineraryFingerprint } from "./fingerprint.js";
16
+ /**
17
+ * Fan out a flight search across an operator's connections, parallelized
18
+ * with per-connection timeout, then merge by itinerary fingerprint.
19
+ *
20
+ * Partial-success semantics: connections that time out / error / report
21
+ * capability-missing are flagged in `perConnection`; the orchestration
22
+ * still returns whatever responding connections produced.
23
+ */
24
+ export async function fanOutFlightSearch(options) {
25
+ const timeoutMs = options.perConnectionTimeoutMs ?? 5000;
26
+ const settled = await Promise.all(options.adapters.map(async ({ connectionId, adapter, context }) => {
27
+ const start = Date.now();
28
+ try {
29
+ // Capability check up front. If the adapter declares a max-slices
30
+ // limit and the request exceeds it, fail fast as `capability_missing`.
31
+ const max = adapter.capabilities.maxSlicesPerSearch;
32
+ if (max != null && options.request.slices.length > max) {
33
+ return {
34
+ connectionId,
35
+ status: "capability_missing",
36
+ offers: [],
37
+ latencyMs: Date.now() - start,
38
+ errorMessage: `Connection supports max ${max} slices; request had ${options.request.slices.length}`,
39
+ };
40
+ }
41
+ const ctx = { connectionId, ...context };
42
+ const response = await withTimeout(adapter.searchFlights(ctx, options.request), timeoutMs, `connection ${connectionId} timed out after ${timeoutMs}ms`);
43
+ return {
44
+ connectionId,
45
+ status: "ok",
46
+ offers: response.offers,
47
+ latencyMs: Date.now() - start,
48
+ };
49
+ }
50
+ catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ const isTimeout = message.includes("timed out");
53
+ return {
54
+ connectionId,
55
+ status: isTimeout ? "timeout" : "error",
56
+ offers: [],
57
+ latencyMs: Date.now() - start,
58
+ errorMessage: message,
59
+ };
60
+ }
61
+ }));
62
+ // Build per-connection status report.
63
+ const perConnection = settled.map((r) => ({
64
+ connectionId: r.connectionId,
65
+ status: r.status,
66
+ count: r.offers.length,
67
+ latencyMs: r.latencyMs,
68
+ errorMessage: r.errorMessage,
69
+ }));
70
+ // Group offers by itinerary fingerprint, building MergedFlightOffer per group.
71
+ const merged = mergeByFingerprint(settled);
72
+ // Sort merged offers by cheapest price ascending.
73
+ merged.sort((a, b) => compareMoney(a.cheapest.totalPrice, b.cheapest.totalPrice));
74
+ const limited = options.limit != null ? merged.slice(0, options.limit) : merged;
75
+ return { offers: limited, perConnection };
76
+ }
77
+ function mergeByFingerprint(results) {
78
+ const buckets = new Map();
79
+ for (const { connectionId, offers } of results) {
80
+ for (const offer of offers) {
81
+ const key = itineraryFingerprint(offer);
82
+ const bucket = buckets.get(key);
83
+ if (bucket) {
84
+ bucket.offers.push({ connectionId, offer });
85
+ bucket.sourceConnectionIds.add(connectionId);
86
+ }
87
+ else {
88
+ buckets.set(key, {
89
+ offers: [{ connectionId, offer }],
90
+ sourceConnectionIds: new Set([connectionId]),
91
+ });
92
+ }
93
+ }
94
+ }
95
+ const merged = [];
96
+ for (const [fingerprint, bucket] of buckets) {
97
+ // Sort offers within the bucket by total price ascending.
98
+ bucket.offers.sort((a, b) => compareMoney(a.offer.totalPrice, b.offer.totalPrice));
99
+ const cheapestEntry = bucket.offers[0];
100
+ if (!cheapestEntry)
101
+ continue;
102
+ merged.push({
103
+ itineraryFingerprint: fingerprint,
104
+ cheapest: cheapestEntry.offer,
105
+ alternates: bucket.offers.slice(1).map((entry) => entry.offer),
106
+ sourceConnectionIds: Array.from(bucket.sourceConnectionIds),
107
+ });
108
+ }
109
+ return merged;
110
+ }
111
+ /**
112
+ * Compare two `Money` values. Currencies must match — cross-currency
113
+ * comparison would require live FX which the orchestration doesn't have.
114
+ * Returns negative if `a < b`, positive if `a > b`, 0 if equal.
115
+ */
116
+ function compareMoney(a, b) {
117
+ if (a.currency !== b.currency) {
118
+ // Fall back to string compare on amount when currencies differ —
119
+ // produces a stable but not-meaningful order. Real deployments
120
+ // normalize currency upstream of the orchestration.
121
+ return a.amount.localeCompare(b.amount);
122
+ }
123
+ const aNum = Number.parseFloat(a.amount);
124
+ const bNum = Number.parseFloat(b.amount);
125
+ return aNum - bNum;
126
+ }
127
+ async function withTimeout(promise, ms, message) {
128
+ return await Promise.race([
129
+ promise,
130
+ new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms)),
131
+ ]);
132
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fan-out.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.test.d.ts","sourceRoot":"","sources":["../../src/orchestration/fan-out.test.ts"],"names":[],"mappings":""}