Skip to content

TS/JS port of fli library - #172

Merged
punitarani merged 7 commits into
mainfrom
claude/fli-python-to-js-nXzUe
May 23, 2026
Merged

TS/JS port of fli library#172
punitarani merged 7 commits into
mainfrom
claude/fli-python-to-js-nXzUe

Conversation

@punitarani

@punitarani punitarani commented May 21, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds a complete TypeScript/JavaScript port of the fli library (fli-js), enabling programmatic access to Google Flights data in Node.js and browser environments. The port is a 1:1 translation of the Python implementation, maintaining API compatibility and byte-identical request/response handling.

Key Changes

  • Core Models (src/models/)

    • Generated Airline and Airport enums from CSV data (2,226+ airlines, 15,784+ airports)
    • Google Flights data models: FlightSearchFilters, FlightResult, FlightLeg, BookingOption, etc.
    • Comprehensive enum types: TripType, SeatType, MaxStops, EmissionsFilter, Currency, Alliance
  • Search Engine (src/search/)

    • SearchFlights: Flight search via GetShoppingResults + GetBookingResults RPC endpoints
    • SearchDates: Date-range search via GetCalendarGraph endpoint
    • HTTP client with rate limiting (10 req/sec), retries, proxy support, and realistic Chrome headers
    • Response decoders for parsing nested-list wire format payloads
    • Protobuf encoder for booking tokens (byte-perfect reproduction of Python implementation)
  • Core Utilities (src/core/)

    • Airport search by IATA code, city name, or airport name (5-priority cascade)
    • Airline/alliance/cabin-class/emissions/max-stops parsers
    • Flight segment and time-restriction builders
    • Currency extraction from price tokens via varint protobuf parsing
  • Comprehensive Test Suite (tests/)

    • Integration tests with stubbed HTTP clients for flights and dates
    • Snapshot tests verifying byte-identical filter formatting vs. Python upstream
    • Unit tests for decoders, protobuf encoding, concurrency primitives, parsers, builders
    • E2E live test (skipped by default, requires FLI_E2E=1)
    • Public API smoke test
  • Build & Configuration

    • Bun-based project with TypeScript strict mode
    • Biome linter + oxlint configuration
    • GitHub Actions workflow for CI/CD
    • Enum generation script mirroring Python's scripts/generate_enums.py

Notable Implementation Details

  • Wire Format Compatibility: Response decoders handle both legacy single-chunk JSONP and multi-chunk formats
  • Protobuf Encoding: Minimal protobuf encoder for booking tokens preserves byte-perfect reproduction of captured live tokens
  • Concurrency: Token-bucket rate limiter adapted to JavaScript's event loop (no threading needed)
  • Filter Formatting: Snapshot tests ensure request payloads are byte-identical to Python client
  • Defensive Parsing: Nested-list accessors (safeGet, asInt, asBool, etc.) handle malformed responses gracefully
  • Enum Generation: Auto-generated from shared data/airlines.csv and data/airports.csv files

Integration

  • Updated .github/workflows/test.yml to run fli-js workflow alongside existing Python tests
  • Shared data files (data/airlines.csv, data/airports.csv) used by both Python and TypeScript implementations
  • Public API exports via src/index.ts for clean consumer-facing interface

https://claude.ai/code/session_01R2vZihmSWV2wvRawjF4Gjo

Greptile Summary

This PR adds a complete TypeScript/JavaScript port of the fli library (fli-js), providing programmatic access to Google Flights data for Node.js and browser environments. It is a 1:1 translation of the Python implementation covering HTTP client, wire-format parsing, protobuf encoding, response decoders, and an extensive test suite with snapshot, integration, and unit tests.

  • Core search engine (src/search/): SearchFlights and SearchDates classes with rate-limited HTTP client, multi-chunk JSONP wire parser, booking-token protobuf encoder/decoder, and round-trip/multi-city expansion via bounded parallelMap.
  • Models and enums (src/models/): Auto-generated Airline (2,226+ entries) and Airport (15,784+ entries) enums from shared CSV data, plus FlightSearchFilters, DateSearchFilters, and all supporting types.
  • Build & CI (fli-js.yml): Bun-based project with TypeScript strict mode, Biome + oxlint, and a CI workflow that verifies generated enums are in sync with the data files before running tests.

Confidence Score: 5/5

This is a well-structured new library with no regressions to existing Python code. All three issues flagged in previous review rounds are correctly resolved in this revision.

All three previously-flagged bugs are fixed: formatDateOnly now uses local-time getters, getClient replaces the singleton when options are passed, and varint decoding uses 2**shift instead of bitwise operators. The remaining findings are style/quality observations that do not affect correctness on any normal code path.

fli-js/src/search/client.ts — abort-signal listener accumulates per retry without removal; fli-js/src/search/decoders.ts — dead first branch in safeAirline.

Important Files Changed

Filename Overview
fli-js/src/search/client.ts HTTP client with rate-limiting, retries, and proxy support. Fixed singleton option-overriding issue from prior review. Minor: abort-signal listener accumulates per retry attempt without removal.
fli-js/src/search/decoders.ts Response decoders for flight search and booking RPC payloads. Correctly uses local-time Date constructor. Dead branch in safeAirline for digit-prefixed IATA codes (fallback handles it correctly).
fli-js/src/search/proto.ts Protobuf encoder/decoder for booking tokens. Fixed bitwise
fli-js/src/search/flights.ts Flight search orchestrator. UTC/local-time mismatch in formatDateOnly fixed. Correct main[0..17] slice for booking payload per Python reference.
fli-js/src/search/wire.ts Wire format parser for multi-chunk JSONP. chunkBytes = length-1 is correct per Python reference: length counts both surrounding newlines.
fli-js/src/search/concurrency.ts Token-bucket rate limiter and bounded parallelMap. FIFO promise-chain serialization correctly implemented for JS event loop.
fli-js/src/models/google-flights/flights.ts FlightSearchFilters model. formatDateOnly now uses local-time getters, fixing the previously-flagged UTC bug.
fli-js/src/search/dates.ts Date-range search via GetCalendarGraph. Chunking logic correctly shifts segment travel_date by MAX_DAYS_PER_SEARCH*chunkIndex.
fli-js/src/core/currency.ts Currency extraction from price token protobuf payload. Uses 2**shift arithmetic correctly. LRU-ish cache bounded at 256 entries.
.github/workflows/fli-js.yml CI workflow for fli-js with lint, typecheck, enum-sync, and test steps. Correctly path-scoped to fli-js/** and data/**.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant SearchFlights
    participant Client
    participant TokenBucket
    participant GoogleAPI
    participant WireParser
    participant Decoders

    Caller->>SearchFlights: search(filters, options)
    SearchFlights->>Client: post(url, f.req body)
    Client->>TokenBucket: acquire()
    TokenBucket-->>Client: token granted
    Client->>GoogleAPI: POST GetShoppingResults
    GoogleAPI-->>Client: multi-chunk JSONP
    Client-->>SearchFlights: ClientResponse
    SearchFlights->>WireParser: parseFirstWrbPayload(text)
    WireParser-->>SearchFlights: inner JSON array
    SearchFlights->>Decoders: parseFlightRow(row) xN
    Decoders-->>SearchFlights: FlightResult[]
    SearchFlights-->>Caller: FlightResult[] or FlightResult[][]
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
fli-js/src/search/decoders.ts:57-65
**Dead branch in `safeAirline` for digit-prefixed IATA codes**

`AIRLINE_BY_CODE` is built by stripping the leading `_` from every key in `AIRLINE_NAMES`, so its keys are always bare IATA codes (e.g. `"2B"`, never `"_2B"`). For digit-prefixed codes, `lookup` is set to `"_2B"`, making the first `if (lookup in AIRLINE_BY_CODE)` branch permanently false. The function falls through to the second check (`if (code in AIRLINE_BY_CODE)`) which finds `"2B"` correctly — so the output is right — but the `lookup` variable and first branch are dead code that will mislead future maintainers.

### Issue 2 of 4
fli-js/src/search/client.ts:172-176
**Abort-signal listener accumulates on each retry and is never removed**

`externalSignal.addEventListener` is called once per attempt inside the retry loop. After a failed attempt, the old controller has already settled, but the listener remains on `externalSignal` until the signal itself is garbage-collected. For a 3-retry request, three listeners accumulate, holding stale controller closures. Removing the listener in `finally` would eliminate the leak.

```suggestion
      let abortListener: (() => void) | undefined;
      if (externalSignal) {
        if (externalSignal.aborted) controller.abort(externalSignal.reason);
        else {
          abortListener = () => controller.abort(externalSignal.reason);
          externalSignal.addEventListener("abort", abortListener);
        }
      }
```

### Issue 3 of 4
fli-js/src/search/client.ts:218-220
Companion cleanup — `abortListener` should be removed in the `finally` block alongside `clearTimeout`, otherwise the listener leaks as described above.

```suggestion
      } finally {
        clearTimeout(timer);
        if (abortListener && externalSignal) {
          externalSignal.removeEventListener("abort", abortListener);
        }
      }
```

### Issue 4 of 4
fli-js/src/search/proto.ts:224-236
**`extractBookingTokenFromTfu` throws a misleading error on unparseable URLs**

When `value.includes("tfu=")` is true but `new URL(value)` throws (e.g. a relative path like `/flights?tfu=ABC`), the catch rethrows `"URL has no 'tfu' query parameter"` — the same message used when the parameter is simply absent. A caller cannot distinguish a malformed URL from one that genuinely lacks the parameter.

Reviews (2): Last reviewed commit: "fli-js: fix UTC/local date, getClient op..." | Re-trigger Greptile

1:1 port of the Python fli library's core (models, core, search) to
TypeScript, exposed as a Bun-managed npm package at fli-js/. The CLI and
MCP server are intentionally out of scope.

Architecture
------------
- Native fetch HTTP client (Bun built-in) with Chrome-like headers,
  TokenBucketRateLimiter (10 req/s), exponential-backoff retries, typed
  SearchClientError family, and HTTPS_PROXY/HTTP_PROXY env support —
  replaces curl_cffi's TLS impersonation with header + proxy-based
  configuration.
- Promise-based concurrency: TokenBucketRateLimiter is an async-await
  variant; parallelMap is a bounded Promise.all with FIFO worker queue.
- Wire format: UTF-8 byte-accurate iterWrbChunks (single + multi chunk),
  hand-rolled varint encoder/decoder reproduces a captured live booking
  token byte-for-byte.
- Models use const-object enum pattern (Airport.JFK === "JFK") with
  generated airport.ts / airline.ts from data/*.csv via
  scripts/generate-enums.ts. Zod schemas drive validation for the
  filter input types.

Tooling
-------
- Bun as package manager + test runner.
- Biome for formatting and base lint, oxlint as the secondary lint pass.
- TypeScript strict mode + noUncheckedIndexedAccess.
- Script entry points (bun run ...): generate:enums, typecheck, format,
  lint, lint:fix, test, test:e2e, ci.

CI
--
- New .github/workflows/fli-js.yml runs format-check, biome + oxlint,
  typecheck, and bun test on changes to fli-js/ or data/. Verifies
  generated enums stay in sync with data/*.csv.
- .github/workflows/test.yml gains an fli-js job that calls the new
  reusable workflow alongside the existing Python jobs.

Tests
-----
252 tests across 17 files (unit + integration + gated e2e):

- Unit: wire, proto (byte-perfect captured-token reproduction), URL
  locale params, helpers, parsers, currency token extraction, airport
  search, decoders, concurrency primitives, HTTP client (stubbed).
- Integration: FlightSearchFilters.format() / DateSearchFilters.format()
  snapshot tests verify byte-identical request payloads vs the Python
  upstream's test fixtures; SearchFlights/SearchDates run end-to-end
  against a stubbed fetch; public-API smoke test of the top-level
  barrel.
- E2E: tests/e2e/live_search.test.ts is gated behind FLI_E2E=1 and
  skipped by default; CI does not invoke it.
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Test Results

    5 files  +  1     56 suites  +52   54s ⏱️ - 1m 3s
  659 tests +272    659 ✅ +272  0 💤 ±0  0 ❌ ±0 
1 861 runs  +313  1 861 ✅ +313  0 💤 ±0  0 ❌ ±0 

Results for commit 67f86e5. ± Comparison against base commit 7327c22.

♻️ This comment has been updated with latest results.

Comment thread fli-js/src/search/flights.ts
Comment thread fli-js/src/models/google-flights/flights.ts
Comment thread fli-js/src/search/client.ts
Comment thread fli-js/src/search/proto.ts
- formatDateOnly (models/google-flights/flights.ts): switch UTC getters
  to local-time getters. parseDateTime in decoders.ts builds Date via
  the local-time `new Date(y, m-1, d, h, min)` constructor; reading back
  with getUTC* would shift the date by ±1 day for any caller not in UTC,
  causing the wrong departure date to be embedded in the selected_flight
  payload for return-leg lookups.

- getClient (search/client.ts): when options are passed after the
  singleton already exists, replace the cached instance instead of
  silently discarding them. Document the behaviour and steer callers
  who need an isolated client toward `new Client(...)` + explicit
  injection.

- _readVarint (search/proto.ts) / readVarint (core/currency.ts): replace
  `value |= chunk << shift` with `value += chunk * 2**shift`. JavaScript
  bitwise ops coerce to signed 32-bit, so any varint encoding past
  bit 30 (value ≥ 2^31) would silently flip into a negative number.
  Throw when the decoded value exceeds Number.MAX_SAFE_INTEGER.

Adds regression tests:
- proto.test.ts: _readVarint decodes 2^31, 2^32, 2^40 correctly.
- filter_format_snapshots.test.ts: a Dec 15 20:00 local-time flight
  encodes "2026-12-15" in the selected_flight payload (would be
  "2026-12-16" with the old UTC-getter behaviour in TZ < UTC).
@punitarani punitarani changed the title Add TypeScript/JavaScript port of fli library TS/JS port of fli library May 21, 2026
punitarani and others added 5 commits May 21, 2026 01:13
Code-review round 2 — addresses the Greptile findings plus issues
surfaced by parallel multi-agent review of the new port.

Real bugs:
- scripts/generate-enums.ts: naive `indexOf(",")` parser silently
  corrupted 10 enum entries whose CSV names contain a comma or
  escaped quote (e.g. `Y2,"Air Century, S.A."` and PAQ's
  `"Warren ""Bud"" Woods…"`). Python uses csv.DictReader which is
  RFC4180-compliant. Replace with a minimal stateful parser that
  understands quoted fields and `""` quote-escaping, then regenerate
  airline.ts and airport.ts so the wire-format sort keys match
  Python again. Add enum-test pins for the previously-broken codes.
- search/dates.ts parseIsoDate: `Number.parseInt("abc",10)` returns
  NaN and `NaN == null` is false, so the existing nullish-only guard
  let malformed dates through and produced an Invalid Date whose
  `.getTime()` silently propagated as NaN. Tighten to Number.isFinite
  and add a regression pin via SearchDates._parseDate.
- search/client.ts: per-attempt `externalSignal.addEventListener` was
  never removed, so a 3-retry request leaked 3 abort closures on the
  shared signal. Store the listener and removeEventListener in the
  attempt's finally block alongside clearTimeout.
- search/client.ts: `await response.text().catch(() => "")` was read
  and immediately dropped via the no-op template `${text ? "" : ""}`.
  Remove the dead read.

Quality cleanups:
- search/decoders.ts safeAirline: drop the dead digit-prefix branch.
  AIRLINE_BY_CODE keys are bare codes (the `_` is stripped at lookup-
  table build), so the `_${code}` lookup was always false and only
  the fallthrough match ran.
- search/proto.ts extractBookingTokenFromTfu: distinguish URL-parse
  failure from `tfu` param absence in the error message so callers
  can tell a malformed input from a well-formed URL that just lacks
  the param.

Local CI green: format, lint, typecheck, 258/258 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two parity gaps surfaced by additional review:

- search/decoders.ts parseDateTime: a partial-null date array like
  `[2026, null, null]` slipped past the existing all-null guard, was
  defaulted to `[2026, 0, 0]`, and `new Date(2026, -1, 0, ...)`
  silently returned Nov 30 2025. Python's `datetime(2026, 0, 0)`
  raises ValueError and the caller treats it as "skip row." Restore
  that strictness with an explicit null/range check on y/m/d. Time
  components keep the `?? 0` fallback to match Python's `or 0`.

- search/wire.ts iterWrbChunks: `Number.parseInt("12abc", 10)`
  returns 12, so a malformed length header would silently shift the
  cursor into garbage instead of aborting the loop. Python's
  `int(raw[cursor:end])` raises and breaks. Add a `/^[0-9]+$/` test
  before parsing so we match Python's all-or-nothing semantics.

Pin both with regression tests in decoders.test.ts. 259/259 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
)

* ci: add npm publish + release flow for fli-js (mirrors PyPI)

Adds a parallel manual-dispatch release pipeline for the fli-js (npm) package
that matches the shape of the existing PyPI flow: release-npm.yml bumps
fli-js/package.json + refreshes bun.lock, commits/tags fli-js-vX.Y.Z, creates
a GitHub Release, then calls publish-npm.yml which runs the JS test suite,
builds via tsc -p tsconfig.build.json, packs, and uploads to npm with
--provenance under the 'npm' GitHub environment using NPM_TOKEN.

- scripts/bump_version.py: generalise to handle package.json via a new
  --package-json flag (text-mode rewrite so formatting is preserved); add
  --tag-prefix so the JS release uses fli-js-v while PyPI keeps v.
- tests/scripts/test_bump_version.py: cover the new JSON path, the
  --tag-prefix flag, and the pyproject/package-json mutual exclusion.
- fli-js: switch package.json to dist-rooted exports, add a tsconfig.build
  with rewriteRelativeImportExtensions so .ts source imports compile to .js
  (and .d.ts) cleanly, add LICENSE, files whitelist, sideEffects: false,
  provenance publishConfig, and a prepublishOnly hook.
- publish.yml: skip on fli-js-v* release events so a JS tag does not try to
  republish to PyPI (and vice-versa for publish-npm.yml).
- dependabot: cover fli-js npm deps weekly with grouped updates.
- docs: add docs/guides/release-npm.md and link from CLAUDE.md, release.md,
  mkdocs nav.

* address greptile review: artifact versions, abort discrimination, parseIsoDate dedupe, JSON depth-aware version replace

- publish-npm.yml: align upload-artifact + download-artifact to @v8 so the
  tarball produced by release-build is guaranteed to be readable by
  npm-publish.
- search/client.ts: distinguish an external AbortSignal from the internal
  timeout. When the caller's signal triggered the abort we now propagate
  the original error (or the caller-supplied reason) as-is, without
  retrying and without relabelling it as SearchTimeoutError — so a
  consumer using `instanceof SearchTimeoutError` to decide whether to
  retry no longer retries on a deliberate cancellation. Adds two
  regression tests covering pre-aborted and mid-flight cancellation.
- core/dates.ts (new): canonical parseIsoDate + formatIsoDate + strict
  ISO_DATE_RE in a shared module. The three near-identical copies in
  search/dates.ts, models/google-flights/base.ts, and
  models/google-flights/dates.ts (one of which threw `Error` instead of
  `TypeError` and skipped range validation) are replaced with imports.
  Updates one assertion in search_dates_stubbed.test.ts whose regex was
  tied to the looser old message.
- bump_version.py: replace the regex-based package.json version rewriter
  with a depth-tracking JSON walker so a nested key literally named
  "version" (e.g. inside `dependencies` or `overrides`) is never
  mistaken for the top-level field. Adds three edge-case tests:
  before-top-level nested key, inline nested object, and a string value
  containing the substring "version".

---------

Co-authored-by: Claude <noreply@anthropic.com>
…js-nXzUe

# Conflicts:
#	.github/workflows/publish.yml
@punitarani
punitarani merged commit 3f3a813 into main May 23, 2026
12 checks passed
@punitarani
punitarani deleted the claude/fli-python-to-js-nXzUe branch May 23, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants