@signalk/tracks-plugin 3.0.3 → 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.
@@ -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 { S as uncoveredBetween, _ as TimeWindowError, a as createDistanceTo, b as thin, c as gpxFilename, d as rfc8187, g as validateParameters, h as unwrapString, i as contextName, l as historyRowPosition, m as trackLabel, n as SelfPositionUnavailableError, p as toIsoTimes, r as asciiFilename, s as createMatcher, t as QueryParameterError, u as resolveContext, v as parseTrackQuery, x as thinToBudget, y as segment } from "./utils-DOEyuPRy.js";
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: toGeoBounds(query.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 contexts = Object.entries(collection).filter(([context]) => !wanted || wanted.has(context));
255
- const reconciled = await Promise.all(contexts.map(async ([context, stored]) => [context, await deps.reconcileWithHistory(context, stored, window, resolution)]));
256
- for (const [context, points] of reconciled) {
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
  }
@@ -1415,7 +1427,8 @@ var Tracks = class {
1415
1427
  this.debug(params);
1416
1428
  this.debug("Self position", selfPosition);
1417
1429
  const matcher = createMatcher(params, selfPosition, debug);
1418
- const contexts = Object.keys(this.tracks);
1430
+ const requested = params.contexts ? new Set(params.contexts) : void 0;
1431
+ const contexts = Object.keys(this.tracks).filter((context) => !requested || requested.has(context));
1419
1432
  return (await Promise.all(contexts.map((context) => this.getTimed(context, query?.window).then((points) => ({
1420
1433
  context,
1421
1434
  points: thin(points, query?.resolution)
@@ -1644,6 +1657,8 @@ function withTimeout(promise, ms) {
1644
1657
  });
1645
1658
  });
1646
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));
1647
1662
  /**
1648
1663
  * Whether a history provider holds anything at all for a context.
1649
1664
  *
@@ -1667,7 +1682,7 @@ async function historyKnowsContext(app, context, debug, cache) {
1667
1682
  const contexts = withTimeout(getHistoryApi(app.config?.settings?.historyApi?.defaultProvider).then((historyApi) => historyApi.getContexts ? historyApi.getContexts({
1668
1683
  from: Temporal.Instant.from((/* @__PURE__ */ new Date(EXISTENCE_PROBE_FROM_MS)).toISOString()),
1669
1684
  to: Temporal.Instant.from(new Date(now).toISOString())
1670
- }) : []).then((listed) => new Set((listed ?? []).filter((c) => typeof c === "string").map((c) => c === "vessels.self" ? app.selfContext : c))), HISTORY_QUERY_TIMEOUT_MS);
1685
+ }) : []).then((listed) => toContextSet(listed, app.selfContext)), HISTORY_QUERY_TIMEOUT_MS);
1671
1686
  const entry = {
1672
1687
  at: now,
1673
1688
  contexts
@@ -1687,6 +1702,38 @@ async function historyKnowsContext(app, context, debug, cache) {
1687
1702
  }
1688
1703
  }
1689
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
+ /**
1690
1737
  * Positions a history provider holds for a window, or none.
1691
1738
  *
1692
1739
  * Best-effort by design: a provider that is absent, slow, or failing must not
@@ -1717,7 +1764,7 @@ async function historyPositions(app, context, window, resolutionMs, debug) {
1717
1764
  try {
1718
1765
  const response = await withTimeout(historyApi.getValues({
1719
1766
  context,
1720
- 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()),
1721
1768
  to: Temporal.Instant.from(new Date(window.to).toISOString()),
1722
1769
  pathSpecs: [{
1723
1770
  path: "navigation.position",
@@ -1885,6 +1932,7 @@ function ThePlugin(app) {
1885
1932
  selfContext: () => app.selfContext,
1886
1933
  segmentGap: () => segmentGap,
1887
1934
  contextName: (context) => contextName(context, remembering(() => tracks)),
1935
+ historyContexts: (window) => historyContextsIn(app, window, app.debug, knownContexts),
1888
1936
  reconcileWithHistory: async (context, stored, window, resolutionMs) => {
1889
1937
  const effective = resolutionMs ?? storeResolution;
1890
1938
  const history = await historyPositions(app, context, window ?? windowSpanning(stored, WINDOWLESS_HISTORY_SPAN_MS), effective, app.debug);