@signalk/tracks-plugin 3.0.0-beta.5 → 3.0.0-beta.7

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.
@@ -34,6 +34,8 @@ export interface SqliteStoreConfig {
34
34
  export declare class SqliteTrackStore implements TrackStore {
35
35
  private readonly db;
36
36
  private readonly insert;
37
+ private readonly insertName;
38
+ private readonly selectName;
37
39
  private readonly maxCells;
38
40
  private readonly segmentGap;
39
41
  private readonly retention;
@@ -92,6 +94,8 @@ export declare class SqliteTrackStore implements TrackStore {
92
94
  * straight line across the anchorage.
93
95
  */
94
96
  segments(context: Context, window?: TimeWindow): LatLngTuple[][];
97
+ recordName(context: Context, name: string, timestamp?: number): void;
98
+ nameFor(context: Context): string | undefined;
95
99
  prune(maxAge: number, keep?: Context): void;
96
100
  close(): void;
97
101
  /** Unsigned round-trip check, used by the tests. */
package/dist/store.d.ts CHANGED
@@ -51,6 +51,21 @@ export interface TrackStore {
51
51
  * "vessels currently near here" rather than "vessels that ever passed here".
52
52
  */
53
53
  getFilteredTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TrackCollection>;
54
+ /**
55
+ * Remember the name a context is currently known by.
56
+ *
57
+ * Names are resolved live from the server's data model on every request,
58
+ * because an AIS static report can arrive long after the first position. But
59
+ * the model is empty after a restart, so a track recorded for months would
60
+ * show as a bare MMSI until the vessel next transmits — and never again for
61
+ * one that has since left. Storing what was known closes that gap.
62
+ *
63
+ * `timestamp` dates the observation so a later report replaces an earlier
64
+ * one rather than the first sighting winning forever.
65
+ */
66
+ recordName(context: Context, name: string, timestamp?: number): void;
67
+ /** The last name stored for a context, or undefined if none ever was. */
68
+ nameFor(context: Context): string | undefined;
54
69
  /**
55
70
  * Drop contexts whose newest position is older than `maxAge` ms.
56
71
  *
@@ -24,7 +24,7 @@ export interface TrackQuery {
24
24
  * `timespan`/`timespanOffset` pair. Absent all of them, no window is applied
25
25
  * and the whole retained track is returned, as before.
26
26
  */
27
- export declare function parseTrackQuery(query: Record<string, unknown>, now?: number): TrackQuery;
27
+ export declare function parseTrackQuery(query: Record<string, unknown>, now?: number, alsoAllowed?: readonly string[]): TrackQuery;
28
28
  /**
29
29
  * Drop points closer together than `resolution` milliseconds, always keeping
30
30
  * the first and last so the track keeps its extent.
package/dist/tracks.d.ts CHANGED
@@ -47,6 +47,16 @@ export declare class Tracks implements TrackStore {
47
47
  * which tracks match — the filter runs once, here.
48
48
  */
49
49
  getFilteredTimedTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TimedTrackCollection>;
50
+ /**
51
+ * Remembered names, by context.
52
+ *
53
+ * In memory only: this store is the client-side accumulator, so there is no
54
+ * restart for a stored name to survive. It implements the same contract so
55
+ * the two stores stay interchangeable and the plugin needs no special case.
56
+ */
57
+ private readonly names;
58
+ recordName(context: Context, name: string, timestamp?: number): void;
59
+ nameFor(context: Context): string | undefined;
50
60
  prune(maxAge: number, keep?: Context): void;
51
61
  }
52
62
  interface AccumulatorParams {
package/dist/utils.d.ts CHANGED
@@ -25,6 +25,25 @@ export declare function createInBounds(bounds: GeoBounds): (position: LatLngTupl
25
25
  * iterate the result.
26
26
  */
27
27
  export declare function splitAtAntimeridian(bounds: GeoBounds): GeoBounds[];
28
+ /**
29
+ * A query parameter was present but malformed.
30
+ *
31
+ * Distinct from absent: a filter nobody asked for is not an error, but one
32
+ * that cannot be honoured must not quietly widen the query. Carries its own
33
+ * message so the handler can hand it to the client, the way `TimeWindowError`
34
+ * already does for the time window.
35
+ */
36
+ export declare class QueryParameterError extends Error {
37
+ }
38
+ /**
39
+ * A well-formed query the server cannot currently answer.
40
+ *
41
+ * `radius` measures from the own vessel, so it needs a self position. Not
42
+ * having one is an outage, not a client mistake -- the same distinction the
43
+ * single-vessel route already draws for a history provider it cannot read.
44
+ */
45
+ export declare class SelfPositionUnavailableError extends Error {
46
+ }
28
47
  export declare function validateParameters(params: QueryParameters, defaultMaxRadius: number | undefined): TrackParams;
29
48
  export declare function createDistanceTo([lat1d, lon1d]: LatLngTuple, debug?: Debug): (d: LatLngTuple | null) => number;
30
49
  export declare function resolveContext(vesselId: string, selfContext?: string): string;
@@ -70,6 +89,8 @@ export declare function contextName(context: string, lookup: (path: string) => u
70
89
  * context, because a label has to render something.
71
90
  */
72
91
  export declare function trackLabel(context: string, selfContext: string | undefined, lookup: (path: string) => unknown): string;
92
+ /** Accepts both a bare string and the `{value}` wrapper deltas arrive in. */
93
+ export declare function unwrapString(raw: unknown): string | undefined;
73
94
  /**
74
95
  * A track label as a filename a browser can save.
75
96
  *
@@ -0,0 +1,94 @@
1
+ # Where a track comes from
2
+
3
+ The plugin can answer a query from two places: its own store, and a history
4
+ provider such as [signalk-questdb](https://www.npmjs.com/package/signalk-questdb)
5
+ or [signalk-to-influxdb2](https://www.npmjs.com/package/signalk-to-influxdb2).
6
+ Neither is simply better than the other, which is why both are used.
7
+
8
+ | | The plugin's store | A history provider |
9
+ | ---------------------------- | ---------------------------------- | ------------------------------------------------ |
10
+ | How often a position is kept | coarse, at a configurable interval | fine, at whatever interval it was configured for |
11
+ | How long it is kept | see below | until its retention drops it |
12
+ | Size | small | far larger, which is why retention exists |
13
+
14
+ The plugin writes its positions to a SQLite file in its data directory, so they
15
+ survive a restart. How long they are kept depends on two settings: the own
16
+ vessel's track is kept indefinitely by default, and another vessel is dropped
17
+ 30 days after its last fix.
18
+
19
+ So the provider is the finer record of the recent past, and the plugin's store
20
+ is what remains of everything older. The interval and the retention are both
21
+ settings on the plugin's configuration page, which is where their current
22
+ defaults are shown.
23
+
24
+ ## What you get with no history provider
25
+
26
+ Everything comes from the plugin's own store, at whatever interval it is
27
+ configured to keep, for as far back as it has been running. Nothing else is needed, and no other plugin has to
28
+ be installed.
29
+
30
+ ```
31
+ query window
32
+ ├──────────────────────────────────────────────────┤
33
+ │ plugin store, at the configured interval │
34
+ ```
35
+
36
+ ## What you get with one
37
+
38
+ The provider answers for the period it covers, and the plugin's store fills
39
+ everything else.
40
+
41
+ ```
42
+ query window: last two years
43
+ ├───────────────────────────────────────┬──────────┤
44
+ │ plugin store, coarse │ provider │
45
+ │ │ finer │
46
+ └ retention begins
47
+ ```
48
+
49
+ A track can therefore change granularity partway along: coarse where it came
50
+ from the store, fine where the provider reached. That is expected. Passing
51
+ `resolution` on a query thins the result to a spacing you choose, which is the
52
+ way to get an evenly spaced track regardless of where each part came from.
53
+
54
+ ## When the provider has holes
55
+
56
+ Retention is only one way a provider's coverage can be incomplete. All of these
57
+ happen, and all are handled the same way:
58
+
59
+ - it was installed after the boat had already been recording
60
+ - it was disabled for a while, or its database was down
61
+ - its retention has dropped the older data
62
+
63
+ In each case the provider simply returns nothing for that period, and the
64
+ plugin's store supplies it instead:
65
+
66
+ ```
67
+ ├─────────┬──────────────┬─────────┬──────────────┤
68
+ │ store │ provider │ store │ provider │
69
+ │ │ │ ↑ provider was down │
70
+ ```
71
+
72
+ Nothing has to be configured for this. The plugin does not read the provider's
73
+ retention setting, or ask how long it has been running — it asks for the window
74
+ and uses what comes back, so coverage that changes underneath needs no
75
+ attention.
76
+
77
+ ## Why the two never double up
78
+
79
+ A provider aggregates into buckets and stamps each one on its boundary:
80
+ `19:00:00.000`, `19:01:00.000`. The plugin's store keeps the time a fix
81
+ actually arrived: `19:00:01.212`. The same physical position therefore has two
82
+ different timestamps in the two sources, and simply merging them would keep
83
+ both.
84
+
85
+ Instead, each bucket of time is filled from exactly one source: the provider
86
+ where it has a position, the store everywhere else. Individual points are never
87
+ compared, so there is nothing to get wrong.
88
+
89
+ ## If a vessel was not moving
90
+
91
+ Neither source has anything, and the track is correctly empty for that period
92
+ rather than filled in. A gap in a track means the vessel was not being
93
+ recorded — which, with the **Pause recording while navigation.state is one of**
94
+ setting, may be deliberate.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/tracks-plugin",
3
- "version": "3.0.0-beta.5",
3
+ "version": "3.0.0-beta.7",
4
4
  "description": "Plugin to keep track of tracks of vessels",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,12 +14,17 @@
14
14
  "types": "./dist/index.d.ts",
15
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.",
16
16
  "files": [
17
+ "app-icon.svg",
17
18
  "dist",
19
+ "docs",
18
20
  "public"
19
21
  ],
20
22
  "signalk": {
21
23
  "displayName": "Tracks",
22
- "appIcon": "./app-icon.svg"
24
+ "appIcon": "./app-icon.svg",
25
+ "screenshots": [
26
+ "./docs/screenshots/tracks_1.png"
27
+ ]
23
28
  },
24
29
  "keywords": [
25
30
  "signalk-node-server-plugin",