@signalk/tracks-plugin 3.0.0-beta.0 → 3.0.0-beta.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.
@@ -55,16 +55,20 @@ export declare class SqliteTrackStore implements TrackStore {
55
55
  track: LatLngTuple[];
56
56
  }[]>;
57
57
  /**
58
- * Tracks whose last position matches the spatial predicate.
58
+ * Tracks matching the spatial predicate, which depends on who is asking.
59
+ *
60
+ * `intersects: false` — the v1 routes — matches a track's *last* position:
61
+ * "which vessels are near me now". `intersects: true` — the v2 Track API —
62
+ * matches a track that passed through the box at any point in the window,
63
+ * including one that has since left.
59
64
  *
60
65
  * Two filters run, and they are not redundant despite both testing bounds:
61
66
  *
62
67
  * - `contextsInBounds` asks the cell index which contexts have *any* position
63
68
  * near the box. It is a cheap prefilter that keeps the query off every row
64
69
  * in the table, and it decides nothing on its own.
65
- * - `matcher` — the same predicate the in-memory store uses — then tests each
66
- * track's *last* position, which is what the endpoint actually means by
67
- * "in this box". That test is authoritative.
70
+ * - `matcher` — the same predicate the in-memory store uses — then applies
71
+ * whichever rule the caller asked for. That test is authoritative.
68
72
  *
69
73
  * Because `matcher` has the final say, dropping the exact-bounds check inside
70
74
  * `contextsInBounds` does not change any result; it only widens the candidate
@@ -88,7 +92,7 @@ export declare class SqliteTrackStore implements TrackStore {
88
92
  * straight line across the anchorage.
89
93
  */
90
94
  segments(context: Context, window?: TimeWindow): LatLngTuple[][];
91
- prune(maxAge: number): void;
95
+ prune(maxAge: number, keep?: Context): void;
92
96
  close(): void;
93
97
  /** Unsigned round-trip check, used by the tests. */
94
98
  cellIdsFor(context: Context): bigint[];
@@ -1,8 +1,4 @@
1
1
  import { Context } from './types.js';
2
- /**
3
- * Off unless a user opts in, per the request and because the states worth
4
- * pausing on depend on how a boat is used.
5
- */
6
2
  export declare const DEFAULT_PAUSE_STATES: string[];
7
3
  /**
8
4
  * The states a boat is plausibly parked in, offered in the plugin UI.
package/dist/store.d.ts CHANGED
@@ -8,12 +8,12 @@ import { TrackQuery } from './timeWindow.js';
8
8
  * calls in `index.ts` and nothing more, so that adding an implementation cannot
9
9
  * quietly widen what a store is expected to do.
10
10
  *
11
- * Note what is *not* here. `getAllTracks` and `getFilteredTracks` are derived
12
- * operations — filtering is a predicate over whole tracks, which the in-memory
13
- * implementation applies in JS and a database implementation will want to push
14
- * into the query. Both stay on the interface for that reason: a store that can
15
- * filter in SQL must be allowed to, rather than being forced to materialise
16
- * every track so a caller can filter it afterwards.
11
+ * `getAllTracks` and `getFilteredTracks` are derived operations, and they are
12
+ * on the interface deliberately: filtering is a predicate over whole tracks,
13
+ * which the in-memory implementation applies in JS and a database
14
+ * implementation will want to push into the query. A store that can filter in
15
+ * SQL must be allowed to, rather than being forced to materialise every track
16
+ * so a caller can filter it afterwards.
17
17
  */
18
18
  export interface TrackStore {
19
19
  /** Record a position for a context. `timestamp` defaults to now. */
@@ -21,7 +21,8 @@ export interface TrackStore {
21
21
  /**
22
22
  * Seed a context's track, replacing whatever it held.
23
23
  *
24
- * Used by the History API bootstrap at startup. `timestamps` is positional
24
+ * A test helper: it is how a known track is installed without going through
25
+ * the position bus, which throttles on write. `timestamps` is positional
25
26
  * against `track`; points without one are dated to the start of time so a
26
27
  * time-window query treats them as older than anything live.
27
28
  */
@@ -50,8 +51,26 @@ export interface TrackStore {
50
51
  * "vessels currently near here" rather than "vessels that ever passed here".
51
52
  */
52
53
  getFilteredTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TrackCollection>;
53
- /** Drop contexts whose newest position is older than `maxAge` ms. */
54
- prune(maxAge: number): void;
54
+ /**
55
+ * Drop contexts whose newest position is older than `maxAge` ms.
56
+ *
57
+ * `keep` is never pruned however long it has been idle. The own vessel's
58
+ * track is the one a user came for — it has to survive a winter on a
59
+ * mooring, a passage with the server off, and any gap in reception. Other
60
+ * vessels age out: a harbour puts hundreds of AIS targets past a receiver in
61
+ * a day, and keeping every one of them forever is not what anybody asked for.
62
+ *
63
+ * An implementation must also apply whatever row-level retention it was
64
+ * configured with, scoped to `keep`. The two are separate: one drops a whole
65
+ * vessel that has gone quiet, the other trims old points from the vessel a
66
+ * user is actually recording, and neither may switch the other off.
67
+ *
68
+ * `Infinity` ages nothing out, which is how a caller asks for the retention
69
+ * pass alone. A negative `maxAge` puts the cutoff in the future and so drops
70
+ * every context — the tests use `-1` for exactly that; the plugin's schema
71
+ * keeps it out of a real configuration.
72
+ */
73
+ prune(maxAge: number, keep?: Context): void;
55
74
  /**
56
75
  * Release any resources held by the store.
57
76
  *
@@ -30,6 +30,24 @@ export declare function parseTrackQuery(query: Record<string, unknown>, now?: nu
30
30
  * the first and last so the track keeps its extent.
31
31
  */
32
32
  export declare function thin(points: TimedPosition[], resolution: number | undefined): TimedPosition[];
33
+ /**
34
+ * Thin to at most `maxPoints`, widening the spacing until the budget fits.
35
+ *
36
+ * A budget, not a fidelity contract: the caller has said how many points it can
37
+ * afford to transfer and draw, whatever shape the track is. Returns the
38
+ * resolution actually applied so the response can report it — a client that is
39
+ * given 29 points instead of the 546 it would otherwise have received should be
40
+ * able to see the spacing that produced them.
41
+ *
42
+ * Widening by doubling from the span-derived estimate rather than binary
43
+ * searching: `thin` keeps the first point and the last unconditionally, so the
44
+ * count is not a clean function of the spacing and an exact solve would be
45
+ * false precision. Two or three passes settle it in practice.
46
+ */
47
+ export declare function thinToBudget(points: TimedPosition[], maxPoints: number, resolution: number | undefined): {
48
+ points: TimedPosition[];
49
+ resolution: number | undefined;
50
+ };
33
51
  /**
34
52
  * Split points into segments wherever recording stopped for longer than `gap`.
35
53
  *
@@ -15,13 +15,15 @@ export type TrackBoundingBox = [number, number, number, number];
15
15
  /**
16
16
  * A request as the server hands it to a provider.
17
17
  *
18
- * Not every field is honoured here. `maxPoints`, `simplify`, `epsilon` and
19
- * `properties` are accepted by the API and ignored by this provider: it serves
20
- * positions, and has no co-recorded values to attach or geometry simplifier to
21
- * run. A caller gets the full, unsimplified track for the window it asked for,
22
- * which is a superset of what it requested rather than a wrong answer. The
23
- * response echoes `resolution` when one was requested, so a client can tell a
24
- * thinned track from a full one.
18
+ * Not every field is honoured here. `simplify`, `epsilon` and `properties` are
19
+ * accepted by the API and ignored by this provider: it serves positions, and
20
+ * has no co-recorded values to attach or geometry simplifier to run. Geometry
21
+ * comes back unsimplified, which is a superset of what such a request asked for
22
+ * rather than a wrong answer.
23
+ *
24
+ * `resolution` and `maxPoints` are both applied, and the response reports the
25
+ * spacing actually used — which is not always the one asked for, since a
26
+ * budget widens it.
25
27
  *
26
28
  * Simplification and co-recorded properties are tracked separately; see
27
29
  * SignalK/tracks.
@@ -1,5 +1,6 @@
1
1
  import { TrackStore } from './store.js';
2
2
  import { TrackApi } from './trackApi.js';
3
+ import { TimedPosition, TimeWindow } from './types.js';
3
4
  /**
4
5
  * Serves the v2 Track API from this plugin's store.
5
6
  *
@@ -17,5 +18,19 @@ export interface TrackProviderDeps {
17
18
  selfContext: () => string;
18
19
  /** Gap in ms that starts a new segment; 0 leaves the track as one line. */
19
20
  segmentGap: () => number;
21
+ /**
22
+ * Positions a history provider holds for a context and window, reconciled
23
+ * with the store's own.
24
+ *
25
+ * Injected rather than reached for directly: the reconciliation belongs to
26
+ * the plugin, and the v1 routes have done it since #73 — a query answered
27
+ * through the v2 provider has to give the same answer as the same query
28
+ * answered through v1, or the plugin contradicts itself depending on which
29
+ * route a client happens to use.
30
+ *
31
+ * Resolves to the store's own points unchanged when no provider is
32
+ * installed, which is the common case.
33
+ */
34
+ reconcileWithHistory: (context: string, stored: TimedPosition[], window: TimeWindow | undefined, resolutionMs: number | undefined) => Promise<TimedPosition[]>;
20
35
  }
21
36
  export declare function createTrackProvider(deps: TrackProviderDeps): TrackApi;
package/dist/tracks.d.ts CHANGED
@@ -15,9 +15,12 @@ export interface TracksConfig {
15
15
  fetchInitialTrack?: boolean;
16
16
  }
17
17
  /**
18
- * The in-memory store: a bounded sliding window of positions per context,
19
- * held in RxJS accumulators and lost on restart. `bootstrapSelfTrack()` in
20
- * index.ts is what re-hydrates it from a History provider at startup.
18
+ * The in-memory store: a bounded sliding window of positions per context, held
19
+ * in RxJS accumulators and lost on restart.
20
+ *
21
+ * No longer used by the plugin, which always records to SQLite. It remains as
22
+ * the client-side `TrackAccumulator` this package exports, where a browser
23
+ * holding one vessel's recent track in memory is exactly the right shape.
21
24
  */
22
25
  export declare class Tracks implements TrackStore {
23
26
  tracks: TracksMap;
@@ -44,7 +47,7 @@ export declare class Tracks implements TrackStore {
44
47
  * which tracks match — the filter runs once, here.
45
48
  */
46
49
  getFilteredTimedTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TimedTrackCollection>;
47
- prune(maxAge: number): void;
50
+ prune(maxAge: number, keep?: Context): void;
48
51
  }
49
52
  interface AccumulatorParams {
50
53
  resolution: number;
package/dist/types.d.ts CHANGED
@@ -48,6 +48,16 @@ export type QueryParameters = Record<string, unknown>;
48
48
  export interface TrackParams {
49
49
  bbox: GeoBounds | null;
50
50
  radius: number | null;
51
+ /**
52
+ * Match a track on *any* position rather than its last.
53
+ *
54
+ * The v1 routes answer "vessels near here now", so they match the last
55
+ * position — a vessel that has left is no longer near. The v2 Track API asks
56
+ * a different question, "tracks that passed through this box within the
57
+ * window", and its contract says a vessel that crossed an hour ago and has
58
+ * since left still matches. Same filter, two questions.
59
+ */
60
+ intersects?: boolean;
51
61
  }
52
62
  export interface Debug {
53
63
  (...args: unknown[]): void;
package/dist/utils.d.ts CHANGED
@@ -29,3 +29,12 @@ export declare function validateParameters(params: QueryParameters, defaultMaxRa
29
29
  export declare function createDistanceTo([lat1d, lon1d]: LatLngTuple, debug?: Debug): (d: LatLngTuple | null) => number;
30
30
  export declare function resolveContext(vesselId: string, selfContext?: string): string;
31
31
  export declare function createMatcher(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug): (track: LatLngTuple[]) => boolean;
32
+ /**
33
+ * A history row is `[timestamp, position]`. Both encodings are legal: the
34
+ * History API types a value as `unknown` and its own comment names "objects
35
+ * (like navigation.position)" alongside primitives, so signalk-questdb returns
36
+ * `{latitude, longitude}` and signalk-to-influxdb2 returns `[lon, lat]`.
37
+ * Accept both — neither is an error, so rejecting one silently yields an empty
38
+ * track rather than a failure anybody can see.
39
+ */
40
+ export declare const historyRowPosition: (row: unknown) => LatLngTuple | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/tracks-plugin",
3
- "version": "3.0.0-beta.0",
3
+ "version": "3.0.0-beta.2",
4
4
  "description": "Plugin to keep track of tracks of vessels",
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,9 +12,15 @@
12
12
  "//main": "Signal K resolves an installed plugin by directory. Node's CJS directory resolution and the server's esm-resolve fallback both read `main`, not `exports`, so dropping this makes the plugin unloadable.",
13
13
  "main": "./dist/index.js",
14
14
  "types": "./dist/index.d.ts",
15
+ "//files": "app-icon.svg ships alongside dist because `files` is an allowlist: the app store probes the declared signalk.appIcon path inside the published tarball, so an icon left out here 404s and the plugin falls back to a placeholder.",
15
16
  "files": [
16
- "dist"
17
+ "dist",
18
+ "app-icon.svg"
17
19
  ],
20
+ "signalk": {
21
+ "displayName": "Tracks",
22
+ "appIcon": "app-icon.svg"
23
+ },
18
24
  "keywords": [
19
25
  "signalk-node-server-plugin",
20
26
  "signalk-category-utility"
@@ -1 +0,0 @@
1
- export {};