Files
2026-05-27 23:29:53 +01:00

4.2 KiB
Raw Permalink Blame History

Design: enterprise-ports

Module layout

packages/geospatial/logistics/ports/
  package.json
  tsconfig.lib.json
  vite.config.ts
  vitest.config.ts
  README.md
  src/
    index.ts
    constants/index.ts                 # EARTH_RADIUS_NM, DEFAULT_SPEED_KNOTS, VESSEL_CLASS_SPEEDS
    types/
      port.types.ts
      port.types.md
    error-codes/
      error-codes.ts
      error-codes.md
      error-codes.test.ts
      index.ts
    schemas/
      schemas.ts
      schemas.md
      schemas.test.ts
      index.ts
    otel/
      otel.ts
      otel.md
      otel.test.ts
      index.ts
    data/
      sea-ports-asia/
      sea-ports-europe/
      sea-ports-americas/
      sea-ports-africa-mena/
      sea-ports-oceania/
      sea-ports/
      airports/
      rail-terminals/
      all-ports/
      shipping-lanes/
    distance/
      distance.ts       # great-circle wrapper, nm/km conversions
      distance.md
      distance.test.ts
      index.ts
    find-port/
      find-port.ts
      find-port.md
      find-port.test.ts
      index.ts
    nearest-port/
      nearest-port.ts
      nearest-port.md
      nearest-port.test.ts
      index.ts
    spatial-index/
      spatial-index.ts
      spatial-index.md
      spatial-index.test.ts
      index.ts
    lane-lookup/
      lane-lookup.ts
      lane-lookup.md
      lane-lookup.test.ts
      index.ts
    lane-distance/
      lane-distance.ts
      lane-distance.md
      lane-distance.test.ts
      index.ts
    lane-progress/
      lane-progress.ts
      lane-progress.md
      lane-progress.test.ts
      index.ts
    eta/
      eta.ts
      eta.md
      eta.test.ts
      index.ts
    property.test.ts

Distance helper

distanceNm(a, b) and distanceKm(a, b) wrap @suaveplan/geo's greatCircleDistance against a WGS84-radius spherical earth (semi-major axis 6_371_000 m) via defineEllipsoid. The genesis greatCircleDistance returns metres by default; this package converts via the geo convertDistance utility. We bind a single EARTH_SPHERE ellipsoid once at module load — no allocation per call.

Error mapping

Each main-branch new PortError("msg", "CODE") call maps to a typed subclass:

main code genesis class
NO_PORTS PortsNoPortsError
INVALID_RADIUS PortsInvalidRadiusError
INSUFFICIENT_WAYPOINTS PortsInsufficientWaypointsError
INVALID_SPEED PortsInvalidSpeedError
MISSING_SPEED PortsMissingSpeedError
(new) PortsPortNotFoundError — strict variant of findPort
(new) PortsLaneNotFoundError — strict variant of getShippingLane
(new) PortsInvalidLaneError — Zod schema rejected

All extend PortsError extends SuaveplanError<PortsCode> with i18n message keys.

Spatial index

PortSpatialIndex keys ports by floor(latitude / 5) and floor(longitude / 5) into a Map<string, Port[]>. Query for a coordinate fetches the 3×3 cell neighbourhood and runs exact great-circle distance on the candidate set. For ~120 curated ports this is overkill in absolute terms but keeps the public API stable as the dataset grows.

OTel instruments

Module-scope meter @suaveplan/ports@0.1.0:

  • Counter ports.lookup_total — labels kind=findPort|byCountry|byType|search.
  • Counter ports.lane_lookup_total — labels mode=sea|rail|air|all.
  • Counter ports.eta_total — labels confidence=high|medium|low.
  • Histogram ports.find_nearest_duration_ms.
  • Histogram ports.lane_distance_duration_ms.
  • Histogram ports.eta_duration_ms.

refreshPortsMeters() re-resolves every instrument against the current global MeterProvider after tests wire in-memory exporters.

Clock injection

estimateETA(coord, lane, speedKnotsOverride?, opts?): opts = { departureTime?: Date; clock?: Clock }. If departureTime is omitted and clock is supplied → new Date(clock.now()); else new Date(). Tests pass a FakeClock from @suaveplan/testing for determinism.

Data shape stability

The migrated dataset preserves every record from the main branch (coordinates, throughputs, vessel limits, IATA/UIC codes) — adding any new fields would break consumer expectations. The data is split into per-region files exactly mirroring the main layout so future curated updates can be diffed cleanly.