diff --git a/src/tools/queries/maritime.js b/src/tools/queries/maritime.js index c86193ac0..5c2107a68 100644 --- a/src/tools/queries/maritime.js +++ b/src/tools/queries/maritime.js @@ -22,6 +22,7 @@ const MMSI_SCHEMA = Object.freeze({ description: 'Maritime Mobile Service Identity, such as "366999712".', }); const MAX_TRACK_POINTS = 200; +const ALL_VESSELS = 1_000_000; const round = (value, digits) => Number.isFinite(value) ? Number(value.toFixed(digits)) : null; @@ -45,6 +46,15 @@ function vesselRow(record, from) { }; } +/** + * Every vessel the source retains. The source's default is the newest few + * thousand, which can leave out a vessel the server still holds; asking for + * more than any server keeps lets the server return all of them. + */ +function readVessels(services, signal) { + return services.vessels.getSnapshot({ maxRows: ALL_VESSELS }, { signal }); +} + function snapshotInfo(snapshot) { return { source: snapshot.source ?? null, @@ -79,7 +89,7 @@ export const vesselsInArea = defineTool({ async run(args, { services, signal }) { const area = await resolveArea(args.area, { services, signal }); const center = areaCenter(area); - const snapshot = await services.vessels.getSnapshot({}, { signal }); + const snapshot = await readVessels(services, signal); const wanted = args.type?.toLowerCase(); const rows = snapshot.records .filter( @@ -130,7 +140,7 @@ export const findVessel = defineTool({ imo: (record) => String(record.imo).replace(/^IMO/i, '') === wanted, name: (record) => String(record.name).toUpperCase().includes(wanted), }[key]; - const snapshot = await services.vessels.getSnapshot({}, { signal }); + const snapshot = await readVessels(services, signal); const rows = snapshot.records .filter(matches) .map((record) => vesselRow(record)); diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index 00211992c..21cdad630 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -2,6 +2,8 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { composeCatalog, coreTools } from '../index.js'; import { LiveSourceError } from '../../sources/live/contract.js'; +import { createAisStreamSource } from '../../sources/live/standalone.js'; +import { AISSTREAM_CACHE_MAX } from '../../../server/providers/vessels/ais-store.js'; const vessel = (id, latitude, longitude, extra = {}) => ({ id, @@ -179,3 +181,23 @@ test('a missing AIS key surfaces as an unavailable feed', async () => { error.message === 'AISSTREAM_API_KEY not set', ); }); + +test('vessel searches ask for every vessel the server retains', async () => { + const requested = []; + const source = createAisStreamSource({ + origin: () => 'http://localhost', + fetchImpl: async (url) => { + requested.push(new URL(url).searchParams.get('maxRows')); + return Response.json({ rows: [], source: 'AISStream', status: 'live' }); + }, + }); + const catalog = composeCatalog({ + tools: coreTools, + services: { vessels: source }, + }); + await catalog.call('find_vessel', { mmsi: '366999712' }); + await catalog.call('vessels_in_area', { area: bay }); + assert.equal(requested.length, 2); + for (const maxRows of requested) + assert.ok(Number(maxRows) >= AISSTREAM_CACHE_MAX, maxRows); +});