@signalk/tracks-plugin 3.0.2 → 3.1.0
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.
- package/dist/harness.test-utils.d.ts +5 -0
- package/dist/index.js +80 -14
- package/dist/index.js.map +1 -1
- package/dist/sqliteStore.d.ts +18 -0
- package/dist/timeWindow.d.ts +32 -4
- package/dist/trackApi.d.ts +6 -0
- package/dist/trackProvider.d.ts +9 -0
- package/dist/trackStoreWorker.js +99 -13
- package/dist/trackStoreWorker.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/{utils-OGjP1B0D.js → utils-CGIGp4Xf.js} +113 -13
- package/dist/utils-CGIGp4Xf.js.map +1 -0
- package/dist/utils.d.ts +13 -0
- package/package.json +1 -1
- package/dist/utils-OGjP1B0D.js.map +0 -1
|
@@ -112,6 +112,11 @@ export interface HarnessOptions {
|
|
|
112
112
|
};
|
|
113
113
|
/** Fail the windowed read, leaving the provider unable to answer at all. */
|
|
114
114
|
getValuesRejects?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Answer `getValues` as a real provider does with `aggregate: 'first'`:
|
|
117
|
+
* only the earliest of `rows` in each bucket of the asked resolution.
|
|
118
|
+
*/
|
|
119
|
+
aggregatesFirst?: boolean;
|
|
115
120
|
/**
|
|
116
121
|
* Reject `getHistoryApi` itself, as the server does when no provider is
|
|
117
122
|
* registered — the default install, and not an outage.
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { C as uncoveredBetween, S as thinToBudget, _ as validateParameters, a as contextName, b as segment, c as createMatcher, d as resolveContext, f as rfc8187, g as unwrapString, h as trackLabel, i as clipToBounds, l as gpxFilename, m as toIsoTimes, n as SelfPositionUnavailableError, o as createDistanceTo, r as asciiFilename, s as createInBounds, t as QueryParameterError, u as historyRowPosition, v as TimeWindowError, x as thin, y as parseTrackQuery } from "./utils-CGIGp4Xf.js";
|
|
2
2
|
import { Temporal } from "@js-temporal/polyfill";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
@@ -241,19 +241,31 @@ function createTrackProvider(deps) {
|
|
|
241
241
|
};
|
|
242
242
|
const window = toTimeWindow(query);
|
|
243
243
|
const resolution = query.resolution ? totalMilliseconds(query.resolution) : void 0;
|
|
244
|
-
const wanted = query.contexts?.length ? new Set(query.contexts.map(resolveSelf(deps.selfContext()))) : void 0;
|
|
244
|
+
const wanted = query.contexts?.length ? [...new Set(query.contexts.map(resolveSelf(deps.selfContext())))] : void 0;
|
|
245
|
+
const bounds = toGeoBounds(query.bbox);
|
|
246
|
+
const clipTo = query.clip === true ? bounds : null;
|
|
245
247
|
const collection = await store.getFilteredTimedTracks({
|
|
246
|
-
bbox:
|
|
248
|
+
bbox: bounds,
|
|
247
249
|
radius: null,
|
|
248
|
-
intersects: true
|
|
250
|
+
intersects: true,
|
|
251
|
+
...wanted ? { contexts: wanted } : {},
|
|
252
|
+
...clipTo ? { clip: true } : {}
|
|
249
253
|
}, void 0, void 0, {
|
|
250
254
|
...window ? { window } : {},
|
|
251
|
-
...resolution === void 0 ? {} : { resolution }
|
|
255
|
+
...resolution === void 0 || bounds ? {} : { resolution }
|
|
252
256
|
});
|
|
257
|
+
const fromHistory = [...await deps.historyContexts(window)].filter((context) => !(context in collection) && (!wanted || wanted.includes(context)));
|
|
258
|
+
const inBounds = bounds ? createInBounds(bounds) : void 0;
|
|
259
|
+
const storedFor = async (context) => {
|
|
260
|
+
const points = await store.getTimed(context, window).catch(() => []);
|
|
261
|
+
return bounds ? points : thin(points, resolution);
|
|
262
|
+
};
|
|
263
|
+
const historyResolution = bounds ? void 0 : resolution;
|
|
253
264
|
const result = /* @__PURE__ */ new Map();
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
const reconciled = await Promise.all([...Object.entries(collection).map(async ([context, stored]) => [context, await deps.reconcileWithHistory(context, stored, window, historyResolution)]), ...fromHistory.map(async (context) => [context, await deps.reconcileWithHistory(context, await storedFor(context), window, historyResolution)])]);
|
|
266
|
+
for (const [context, reconciledPoints] of reconciled) {
|
|
267
|
+
if (inBounds && !reconciledPoints.some(({ position }) => inBounds(position))) continue;
|
|
268
|
+
const points = clipTo ? thin(clipToBounds(reconciledPoints, clipTo), resolution) : bounds ? thin(reconciledPoints, resolution) : reconciledPoints;
|
|
257
269
|
if (points.length === 0) continue;
|
|
258
270
|
result.set(context, points);
|
|
259
271
|
}
|
|
@@ -792,12 +804,29 @@ function reconcile(history, stored, resolution) {
|
|
|
792
804
|
for (const point of stored) if (!covered.has(bucketOf(point.timestamp, resolution))) positions.push(point);
|
|
793
805
|
positions.sort((a, b) => a.timestamp - b.timestamp);
|
|
794
806
|
return {
|
|
795
|
-
positions,
|
|
807
|
+
positions: measuredFromHistory(positions, history),
|
|
796
808
|
fromHistory,
|
|
797
809
|
fromStore: positions.length - fromHistory
|
|
798
810
|
};
|
|
799
811
|
}
|
|
800
812
|
/**
|
|
813
|
+
* A stored point's `pauseBefore` was measured over the stored points thinning
|
|
814
|
+
* dropped before it. Where a history point now comes right before it, history
|
|
815
|
+
* covered part of that time, so the pause is at most the time since that
|
|
816
|
+
* bucket, and at most the longest step the store recorded, whichever is less.
|
|
817
|
+
*/
|
|
818
|
+
function measuredFromHistory(positions, history) {
|
|
819
|
+
const fromHistory = new Set(history);
|
|
820
|
+
return positions.map((point, i) => {
|
|
821
|
+
const before = positions[i - 1];
|
|
822
|
+
if (point.pauseBefore === void 0 || before === void 0 || !fromHistory.has(before)) return point;
|
|
823
|
+
return {
|
|
824
|
+
...point,
|
|
825
|
+
pauseBefore: Math.min(point.pauseBefore, uncoveredBetween(before, point))
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
801
830
|
* Fallback when no bucket width is known: history covers its own time span,
|
|
802
831
|
* and the store supplies whatever falls outside it.
|
|
803
832
|
*
|
|
@@ -818,7 +847,7 @@ function reconcileByExtent(history, stored) {
|
|
|
818
847
|
}
|
|
819
848
|
const outside = stored.filter((p) => p.timestamp < first || p.timestamp > last);
|
|
820
849
|
return {
|
|
821
|
-
positions: [...history, ...outside].sort((a, b) => a.timestamp - b.timestamp),
|
|
850
|
+
positions: measuredFromHistory([...history, ...outside].sort((a, b) => a.timestamp - b.timestamp), history),
|
|
822
851
|
fromHistory: history.length,
|
|
823
852
|
fromStore: outside.length
|
|
824
853
|
};
|
|
@@ -1398,7 +1427,8 @@ var Tracks = class {
|
|
|
1398
1427
|
this.debug(params);
|
|
1399
1428
|
this.debug("Self position", selfPosition);
|
|
1400
1429
|
const matcher = createMatcher(params, selfPosition, debug);
|
|
1401
|
-
const
|
|
1430
|
+
const requested = params.contexts ? new Set(params.contexts) : void 0;
|
|
1431
|
+
const contexts = Object.keys(this.tracks).filter((context) => !requested || requested.has(context));
|
|
1402
1432
|
return (await Promise.all(contexts.map((context) => this.getTimed(context, query?.window).then((points) => ({
|
|
1403
1433
|
context,
|
|
1404
1434
|
points: thin(points, query?.resolution)
|
|
@@ -1627,6 +1657,8 @@ function withTimeout(promise, ms) {
|
|
|
1627
1657
|
});
|
|
1628
1658
|
});
|
|
1629
1659
|
}
|
|
1660
|
+
/** A provider's context list as a set, with the `vessels.self` spelling resolved. */
|
|
1661
|
+
var toContextSet = (listed, selfContext) => new Set((listed ?? []).filter((c) => typeof c === "string").map((c) => c === "vessels.self" ? selfContext : c));
|
|
1630
1662
|
/**
|
|
1631
1663
|
* Whether a history provider holds anything at all for a context.
|
|
1632
1664
|
*
|
|
@@ -1650,7 +1682,7 @@ async function historyKnowsContext(app, context, debug, cache) {
|
|
|
1650
1682
|
const contexts = withTimeout(getHistoryApi(app.config?.settings?.historyApi?.defaultProvider).then((historyApi) => historyApi.getContexts ? historyApi.getContexts({
|
|
1651
1683
|
from: Temporal.Instant.from((/* @__PURE__ */ new Date(EXISTENCE_PROBE_FROM_MS)).toISOString()),
|
|
1652
1684
|
to: Temporal.Instant.from(new Date(now).toISOString())
|
|
1653
|
-
}) : []).then((listed) =>
|
|
1685
|
+
}) : []).then((listed) => toContextSet(listed, app.selfContext)), HISTORY_QUERY_TIMEOUT_MS);
|
|
1654
1686
|
const entry = {
|
|
1655
1687
|
at: now,
|
|
1656
1688
|
contexts
|
|
@@ -1670,6 +1702,38 @@ async function historyKnowsContext(app, context, debug, cache) {
|
|
|
1670
1702
|
}
|
|
1671
1703
|
}
|
|
1672
1704
|
/**
|
|
1705
|
+
* Contexts a history provider holds data for within a window, or none.
|
|
1706
|
+
*
|
|
1707
|
+
* What lets the v2 provider return a vessel the store has never seen, or match
|
|
1708
|
+
* a box on positions only history recorded: the store is often far younger
|
|
1709
|
+
* than the provider. Best-effort like every other provider call: no provider,
|
|
1710
|
+
* a provider without `getContexts`, an error or a hang all answer with no
|
|
1711
|
+
* contexts, and the store answers alone.
|
|
1712
|
+
*
|
|
1713
|
+
* A window with no start is asked from the epoch, for the same reason the
|
|
1714
|
+
* existence probe is; without any window the probe's cached all-time list is
|
|
1715
|
+
* reused rather than asking again per query.
|
|
1716
|
+
*/
|
|
1717
|
+
async function historyContextsIn(app, window, debug, cache) {
|
|
1718
|
+
const getHistoryApi = app.getHistoryApi;
|
|
1719
|
+
if (!getHistoryApi) return /* @__PURE__ */ new Set();
|
|
1720
|
+
try {
|
|
1721
|
+
if (!window) {
|
|
1722
|
+
await historyKnowsContext(app, app.selfContext, debug, cache);
|
|
1723
|
+
return await cache.current?.contexts ?? /* @__PURE__ */ new Set();
|
|
1724
|
+
}
|
|
1725
|
+
const historyApi = await withTimeout(getHistoryApi(app.config?.settings?.historyApi?.defaultProvider), HISTORY_QUERY_TIMEOUT_MS);
|
|
1726
|
+
if (!historyApi.getContexts) return /* @__PURE__ */ new Set();
|
|
1727
|
+
return toContextSet(await withTimeout(historyApi.getContexts({
|
|
1728
|
+
from: Temporal.Instant.from(new Date(Math.max(window.from, EXISTENCE_PROBE_FROM_MS)).toISOString()),
|
|
1729
|
+
to: Temporal.Instant.from(new Date(window.to).toISOString())
|
|
1730
|
+
}), HISTORY_QUERY_TIMEOUT_MS), app.selfContext);
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
if (debug.enabled) debug(`History contexts unavailable: ${errorDetail(err)}`);
|
|
1733
|
+
return /* @__PURE__ */ new Set();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1673
1737
|
* Positions a history provider holds for a window, or none.
|
|
1674
1738
|
*
|
|
1675
1739
|
* Best-effort by design: a provider that is absent, slow, or failing must not
|
|
@@ -1700,7 +1764,7 @@ async function historyPositions(app, context, window, resolutionMs, debug) {
|
|
|
1700
1764
|
try {
|
|
1701
1765
|
const response = await withTimeout(historyApi.getValues({
|
|
1702
1766
|
context,
|
|
1703
|
-
from: Temporal.Instant.from(new Date(window.from).toISOString()),
|
|
1767
|
+
from: Temporal.Instant.from(new Date(Math.max(window.from, EXISTENCE_PROBE_FROM_MS)).toISOString()),
|
|
1704
1768
|
to: Temporal.Instant.from(new Date(window.to).toISOString()),
|
|
1705
1769
|
pathSpecs: [{
|
|
1706
1770
|
path: "navigation.position",
|
|
@@ -1716,7 +1780,8 @@ async function historyPositions(app, context, window, resolutionMs, debug) {
|
|
|
1716
1780
|
const withinEnd = window.inclusiveEnd ? timestamp <= window.to : timestamp < window.to;
|
|
1717
1781
|
if (timestamp >= window.from && withinEnd) points.push({
|
|
1718
1782
|
position,
|
|
1719
|
-
timestamp
|
|
1783
|
+
timestamp,
|
|
1784
|
+
span: applied
|
|
1720
1785
|
});
|
|
1721
1786
|
}
|
|
1722
1787
|
}
|
|
@@ -1867,6 +1932,7 @@ function ThePlugin(app) {
|
|
|
1867
1932
|
selfContext: () => app.selfContext,
|
|
1868
1933
|
segmentGap: () => segmentGap,
|
|
1869
1934
|
contextName: (context) => contextName(context, remembering(() => tracks)),
|
|
1935
|
+
historyContexts: (window) => historyContextsIn(app, window, app.debug, knownContexts),
|
|
1870
1936
|
reconcileWithHistory: async (context, stored, window, resolutionMs) => {
|
|
1871
1937
|
const effective = resolutionMs ?? storeResolution;
|
|
1872
1938
|
const history = await historyPositions(app, context, window ?? windowSpanning(stored, WINDOWLESS_HISTORY_SPAN_MS), effective, app.debug);
|