4.2 KiB
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— labelskind=findPort|byCountry|byType|search. - Counter
ports.lane_lookup_total— labelsmode=sea|rail|air|all. - Counter
ports.eta_total— labelsconfidence=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.