signalk-questdb 2.2.2 → 2.3.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.
Files changed (49) hide show
  1. package/.coderabbit.yaml +19 -15
  2. package/.release-please-manifest.json +1 -1
  3. package/CLAUDE.md +4 -3
  4. package/README.md +94 -5
  5. package/dist/api-contract.d.ts +46 -0
  6. package/dist/archive.d.ts +44 -0
  7. package/dist/archive.js +117 -0
  8. package/dist/archive.js.map +1 -0
  9. package/dist/config/schema.d.ts +3 -0
  10. package/dist/config/schema.js +27 -0
  11. package/dist/config/schema.js.map +1 -1
  12. package/dist/container-env.js +11 -0
  13. package/dist/container-env.js.map +1 -1
  14. package/dist/gpx-import-session.d.ts +69 -0
  15. package/dist/gpx-import-session.js +218 -0
  16. package/dist/gpx-import-session.js.map +1 -0
  17. package/dist/gpx-import.d.ts +135 -0
  18. package/dist/gpx-import.js +452 -0
  19. package/dist/gpx-import.js.map +1 -0
  20. package/dist/history-v1.d.ts +2 -1
  21. package/dist/history-v1.js +77 -19
  22. package/dist/history-v1.js.map +1 -1
  23. package/dist/history-v2.d.ts +2 -1
  24. package/dist/history-v2.js +54 -26
  25. package/dist/history-v2.js.map +1 -1
  26. package/dist/ilp-writer.d.ts +1 -1
  27. package/dist/ilp-writer.js +4 -2
  28. package/dist/ilp-writer.js.map +1 -1
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +239 -10
  31. package/dist/index.js.map +1 -1
  32. package/dist/query-client.js +27 -0
  33. package/dist/query-client.js.map +1 -1
  34. package/dist/retention.d.ts +28 -1
  35. package/dist/retention.js +152 -12
  36. package/dist/retention.js.map +1 -1
  37. package/package.json +2 -1
  38. package/public/assets/PluginConfigurationPanel-DR-y5e3d.js +11 -0
  39. package/public/assets/{_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-D-Le-Wxu.js → _virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-BUtmcsA-.js} +1 -1
  40. package/public/assets/{dist-BI-Ygd_Z.js → dist-C5M3fhBw.js} +1 -1
  41. package/public/assets/helpers-oIxDNnZJ.js +1 -0
  42. package/public/assets/{hostInit-CdqUOHAJ.js → hostInit-B3lIJd9C.js} +1 -1
  43. package/public/assets/{index-mvK71Kw7.js → index-eAjx9ZuX.js} +2 -2
  44. package/public/assets/{pendingShares-BI6kviiM.js → pendingShares-PvyVL1Z8.js} +2 -2
  45. package/public/assets/utils-v9GZlbdk.js +4 -0
  46. package/public/remoteEntry.js +2 -2
  47. package/public/assets/PluginConfigurationPanel-JR1rvS7f.js +0 -9
  48. package/public/assets/helpers-CsBAxMau.js +0 -1
  49. package/public/assets/utils-BRGuweMc.js +0 -4
@@ -0,0 +1,452 @@
1
+ // Importing a GPX track into the archive.
2
+ //
3
+ // A chartplotter's track export runs to gigabytes — a TimeZero export of four
4
+ // years is 1.5 GB and 11 million points — so nothing here holds a document.
5
+ // The browser sends the file in slices, each slice is parsed as it arrives,
6
+ // and its points go to the ILP writer before the next slice is accepted. Memory
7
+ // stays at one slice plus the parser's state, whatever the file's size.
8
+ //
9
+ // Points land in the position archive, the table retention never drops,
10
+ // under their own source, so an import can be told apart from what the
11
+ // server recorded and a second import of the same file upserts onto the
12
+ // first instead of doubling it. They are thinned on the way in to the
13
+ // archive's resolution, by stamping each on the start of its interval and
14
+ // letting the table's dedup keep one per interval: an export's block seams
15
+ // and overlapping tracks then collapse without the importer having to see
16
+ // them.
17
+ import { SaxesParser } from "saxes";
18
+ import { ARCHIVE_TABLES, readFrom } from "./archive.js";
19
+ /** The sourceRef imported positions are stored under. */
20
+ export const IMPORT_SOURCE = "import.gpx";
21
+ // Slices handed to the parser at once. Each runs synchronously, so this
22
+ // bounds how long an import holds the event loop between yields.
23
+ const SLICE_BYTES = 64 * 1024;
24
+ // A point is left out where the vessel already has a recorded position in the
25
+ // same five minutes: interleaving two receivers' fixes draws a sawtooth.
26
+ const COVERAGE_MS = 5 * 60 * 1000;
27
+ const DAY_MS = 24 * 60 * 60 * 1000;
28
+ // Days of coverage kept while an import runs. An export is mostly in time
29
+ // order, so only the days around the current point are ever asked again.
30
+ const COVERAGE_DAYS_CACHED = 8;
31
+ // Track names listed individually in the summary; the rest are counted under
32
+ // one key. The summary goes out with every slice's reply, and a file can hold
33
+ // a whole anchorage's worth of AIS targets.
34
+ const MAX_NAMED = 100;
35
+ const OTHER_NAMES = "(other tracks)";
36
+ const GPX_NAMESPACES = new Set([
37
+ "",
38
+ "http://www.topografix.com/GPX/1/1",
39
+ "http://www.topografix.com/GPX/1/0",
40
+ ]);
41
+ /**
42
+ * An attribute as an `xsd:decimal`, or NaN. `Number()` reads `0x10` and
43
+ * `1e1` too, which GPX does not allow and no exporter means.
44
+ */
45
+ export function gpxDecimal(value) {
46
+ return value !== undefined && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value)
47
+ ? Number(value)
48
+ : Number.NaN;
49
+ }
50
+ /**
51
+ * A `<time>` as epoch milliseconds, or NaN. GPX types it `xsd:dateTime`;
52
+ * `Date.parse` alone takes a bare year and rolls February 30th into March.
53
+ */
54
+ export function gpxDateTime(value) {
55
+ const match = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(value);
56
+ if (!match)
57
+ return Number.NaN;
58
+ const parsed = Date.parse(value);
59
+ if (Number.isNaN(parsed))
60
+ return Number.NaN;
61
+ const year = Number(match[1]);
62
+ const month = Number(match[2]);
63
+ const day = Number(match[3]);
64
+ const calendar = new Date(Date.UTC(year, month - 1, day));
65
+ return calendar.getUTCFullYear() === year &&
66
+ calendar.getUTCMonth() === month - 1 &&
67
+ calendar.getUTCDate() === day
68
+ ? parsed
69
+ : Number.NaN;
70
+ }
71
+ /**
72
+ * A longitude brought into [-180, 180], or NaN when it is not one.
73
+ *
74
+ * Crossing the antimeridian, TimeZero writes the fixes just past it as
75
+ * -180.00002 rather than 179.99998. Those are real positions, so they are
76
+ * wrapped; a value beyond a full turn is not a position at all.
77
+ */
78
+ export function antimeridian(longitude) {
79
+ if (!(longitude > -360 && longitude < 360))
80
+ return Number.NaN;
81
+ if (longitude < -180)
82
+ return longitude + 360;
83
+ if (longitude > 180)
84
+ return longitude - 360;
85
+ return longitude;
86
+ }
87
+ // Names counted so far per tally, so a point past the cap costs no scan of
88
+ // the keys: in an export full of AIS targets that is most points.
89
+ const NAMED = new WeakMap();
90
+ /** Count one point against a track name, bounded at MAX_NAMED names. */
91
+ function tally(counts, name) {
92
+ // Own properties only: a track named "constructor" is a name like any
93
+ // other, not the object's inherited function.
94
+ if (Object.hasOwn(counts, name)) {
95
+ counts[name] += 1;
96
+ return;
97
+ }
98
+ const named = NAMED.get(counts) ?? 0;
99
+ if (named < MAX_NAMED) {
100
+ counts[name] = 1;
101
+ NAMED.set(counts, named + 1);
102
+ return;
103
+ }
104
+ counts[OTHER_NAMES] =
105
+ (Object.hasOwn(counts, OTHER_NAMES) ? counts[OTHER_NAMES] : 0) + 1;
106
+ }
107
+ /** The document is not well-formed XML; rows before the error are kept. */
108
+ export class GpxParseError extends Error {
109
+ }
110
+ /** The import was aborted while a slice was being written. */
111
+ export class GpxImportCancelled extends Error {
112
+ }
113
+ /**
114
+ * One import, fed the document slice by slice.
115
+ *
116
+ * Only GPX's own structure counts: a `trk` under `gpx`, a `trkseg` under it,
117
+ * a `trkpt` under that. A vendor `<trkseg>` inside `<extensions>` is not a
118
+ * segment, and a `<name>` inside a point is not the track's name.
119
+ */
120
+ export class GpxImport {
121
+ options;
122
+ summary = {
123
+ bytes: 0,
124
+ tracks: 0,
125
+ // Without a prototype: a track name is the file's to choose, and one
126
+ // named "__proto__" must be a key like any other, not the setter.
127
+ importedTracks: Object.create(null),
128
+ skippedTracks: Object.create(null),
129
+ points: 0,
130
+ imported: 0,
131
+ thinned: 0,
132
+ alreadyRecorded: 0,
133
+ invalid: 0,
134
+ from: null,
135
+ to: null,
136
+ };
137
+ parser = new SaxesParser({ xmlns: true });
138
+ decoder = new TextDecoder("utf-8", { fatal: true });
139
+ parseError = null;
140
+ cancelled = false;
141
+ // Local names of the open GPX elements, "" for a foreign one, so a
142
+ // structural element is recognised only at its place in the hierarchy.
143
+ path = [];
144
+ trackName = null;
145
+ nameText = "";
146
+ selected = false;
147
+ segment = 0;
148
+ point = null;
149
+ timeText = "";
150
+ pending = [];
151
+ lastSegment = -1;
152
+ last = null;
153
+ lastBucket = Number.NaN;
154
+ coverage = new Map();
155
+ coveredDay = Number.NaN;
156
+ covered = new Set();
157
+ constructor(options) {
158
+ this.options = options;
159
+ this.parser.on("opentag", (tag) => this.open(tag));
160
+ this.parser.on("closetag", () => this.close());
161
+ this.parser.on("text", (text) => this.text(text));
162
+ this.parser.on("cdata", (text) => this.text(text));
163
+ // saxes reports and carries on; an import stops at the first error.
164
+ this.parser.on("error", (err) => {
165
+ this.parseError ??= err;
166
+ });
167
+ }
168
+ /**
169
+ * The instant of the row written last, or null before the first. Not
170
+ * `summary.to`: an export out of time order writes its latest row early.
171
+ */
172
+ get lastWritten() {
173
+ return this.last;
174
+ }
175
+ /** Stop writing: the slice in progress ends at its next point. */
176
+ cancel() {
177
+ this.cancelled = true;
178
+ }
179
+ /** Parse the next bytes of the document and write the points they hold. */
180
+ async write(chunk) {
181
+ for (let at = 0; at < chunk.length; at += SLICE_BYTES) {
182
+ const slice = chunk.subarray(at, at + SLICE_BYTES);
183
+ let text;
184
+ try {
185
+ text = this.decoder.decode(slice, { stream: true });
186
+ }
187
+ catch {
188
+ throw new GpxParseError(`The file is not UTF-8 near byte ${this.summary.bytes}`);
189
+ }
190
+ this.parse(text);
191
+ this.summary.bytes += slice.length;
192
+ // The points read before an error are sound; they go out first.
193
+ await this.drain();
194
+ this.throwParseError();
195
+ await new Promise((resolve) => setImmediate(resolve));
196
+ }
197
+ }
198
+ /** The document has ended: anything left unclosed is an error. */
199
+ async finish() {
200
+ let rest;
201
+ try {
202
+ rest = this.decoder.decode();
203
+ }
204
+ catch {
205
+ throw new GpxParseError("The file ends inside a UTF-8 character");
206
+ }
207
+ this.parse(rest);
208
+ this.parser.close();
209
+ await this.drain();
210
+ this.throwParseError();
211
+ return this.summary;
212
+ }
213
+ parse(text) {
214
+ if (text !== "" && !this.parseError)
215
+ this.parser.write(text);
216
+ }
217
+ throwParseError() {
218
+ if (this.parseError) {
219
+ throw new GpxParseError(`The file is not well-formed XML: ${this.parseError.message}`);
220
+ }
221
+ }
222
+ at(...names) {
223
+ if (this.path.length !== names.length)
224
+ return false;
225
+ return names.every((name, i) => this.path[i] === name);
226
+ }
227
+ open(tag) {
228
+ if (this.parseError)
229
+ return;
230
+ this.path.push(GPX_NAMESPACES.has(tag.uri) ? tag.local : "");
231
+ if (this.at("gpx", "trk")) {
232
+ this.summary.tracks++;
233
+ this.trackName = null;
234
+ this.selected = false;
235
+ }
236
+ else if (this.at("gpx", "trk", "name")) {
237
+ this.nameText = "";
238
+ }
239
+ else if (this.at("gpx", "trk", "trkseg")) {
240
+ // GPX puts <name> before the segments; a track whose name is missing
241
+ // or comes later is selected as the track without one.
242
+ if (this.trackName === null)
243
+ this.name("");
244
+ this.segment++;
245
+ }
246
+ else if (this.at("gpx", "trk", "trkseg", "trkpt")) {
247
+ this.point = {
248
+ latitude: gpxDecimal(tag.attributes.lat?.value),
249
+ longitude: gpxDecimal(tag.attributes.lon?.value),
250
+ };
251
+ this.timeText = "";
252
+ }
253
+ }
254
+ text(text) {
255
+ if (this.at("gpx", "trk", "name"))
256
+ this.nameText += text;
257
+ else if (this.at("gpx", "trk", "trkseg", "trkpt", "time")) {
258
+ this.timeText += text;
259
+ }
260
+ }
261
+ close() {
262
+ // saxes carries on past an error, so the rest of the slice it was in
263
+ // still produces events; none of them is trusted.
264
+ if (this.parseError)
265
+ return;
266
+ if (this.at("gpx", "trk", "name") && this.trackName === null) {
267
+ this.name(this.nameText.trim());
268
+ }
269
+ else if (this.at("gpx", "trk", "trkseg", "trkpt") && this.point) {
270
+ this.summary.points++;
271
+ const { latitude, longitude } = this.point;
272
+ this.point = null;
273
+ const name = this.trackName ?? "";
274
+ if (!this.selected) {
275
+ tally(this.summary.skippedTracks, name);
276
+ }
277
+ else {
278
+ tally(this.summary.importedTracks, name);
279
+ const ms = gpxDateTime(this.timeText.trim());
280
+ const lon = antimeridian(longitude);
281
+ if (Number.isNaN(ms) ||
282
+ !(latitude >= -90 && latitude <= 90) ||
283
+ Number.isNaN(lon)) {
284
+ this.summary.invalid++;
285
+ }
286
+ else {
287
+ this.pending.push({
288
+ ms,
289
+ latitude,
290
+ longitude: lon,
291
+ segment: this.segment,
292
+ });
293
+ }
294
+ }
295
+ }
296
+ this.path.pop();
297
+ }
298
+ name(name) {
299
+ this.trackName = name;
300
+ this.selected = this.options.tracks.has(name);
301
+ }
302
+ async recordedIn(ms) {
303
+ const lookup = this.options.recorded;
304
+ if (!lookup)
305
+ return null;
306
+ const day = Math.floor(ms / DAY_MS) * DAY_MS;
307
+ if (day === this.coveredDay)
308
+ return this.covered;
309
+ let found = this.coverage.get(day);
310
+ if (!found) {
311
+ found = lookup(day);
312
+ this.coverage.set(day, found);
313
+ if (this.coverage.size > COVERAGE_DAYS_CACHED) {
314
+ const oldest = this.coverage.keys().next().value;
315
+ if (oldest !== undefined)
316
+ this.coverage.delete(oldest);
317
+ }
318
+ }
319
+ this.covered = await found;
320
+ this.coveredDay = day;
321
+ return this.covered;
322
+ }
323
+ async drain() {
324
+ if (this.pending.length === 0)
325
+ return;
326
+ const points = this.pending;
327
+ this.pending = [];
328
+ const resolution = Math.floor(this.options.resolutionSeconds) * 1000;
329
+ for (const point of points) {
330
+ if (this.cancelled)
331
+ throw new GpxImportCancelled("The import was aborted");
332
+ const recorded = await this.recordedIn(point.ms);
333
+ if (recorded?.has(Math.floor(point.ms / COVERAGE_MS))) {
334
+ this.summary.alreadyRecorded++;
335
+ continue;
336
+ }
337
+ const bucket = resolution > 0 ? Math.floor(point.ms / resolution) : point.ms;
338
+ if (point.segment === this.lastSegment && bucket === this.lastBucket) {
339
+ this.summary.thinned++;
340
+ continue;
341
+ }
342
+ this.lastSegment = point.segment;
343
+ this.lastBucket = bucket;
344
+ const ms = resolution > 0 ? bucket * resolution : point.ms;
345
+ this.options.write(ms, point.latitude, point.longitude);
346
+ this.last = ms;
347
+ this.summary.imported++;
348
+ const iso = new Date(ms).toISOString();
349
+ if (this.summary.from === null || iso < this.summary.from) {
350
+ this.summary.from = iso;
351
+ }
352
+ if (this.summary.to === null || iso > this.summary.to) {
353
+ this.summary.to = iso;
354
+ }
355
+ }
356
+ await this.options.backpressure();
357
+ }
358
+ }
359
+ // Same marks as the InfluxDB migration: far below the writer's buffer cap,
360
+ // which drops the OLDEST lines once reached — the very rows being imported.
361
+ const WRITER_HIGH_WATER = 20_000;
362
+ const WRITER_RESUME = 5_000;
363
+ const WRITER_TIMEOUT_MS = 60_000;
364
+ const WRITER_POLL_MS = 200;
365
+ /**
366
+ * What an import needs from the shared ILP writer: to wait while it is full,
367
+ * to know when everything written has left it, and to notice when it had to
368
+ * drop lines, which may have been the import's.
369
+ */
370
+ export class WriterGate {
371
+ writer;
372
+ pollMs;
373
+ timeoutMs;
374
+ droppedAtStart;
375
+ constructor(writer, pollMs = WRITER_POLL_MS, timeoutMs = WRITER_TIMEOUT_MS) {
376
+ this.writer = writer;
377
+ this.pollMs = pollMs;
378
+ this.timeoutMs = timeoutMs;
379
+ this.droppedAtStart = writer.droppedLineCount;
380
+ }
381
+ /** Resolves once the writer has room; rejects if it does not drain. */
382
+ async room() {
383
+ this.checkDrops();
384
+ if (this.writer.pendingLines <= WRITER_HIGH_WATER)
385
+ return;
386
+ await this.until(() => this.writer.pendingLines <= WRITER_RESUME, "QuestDB is not accepting writes");
387
+ }
388
+ /** Resolves once every line enqueued so far has left the writer. */
389
+ async settled() {
390
+ const mark = this.writer.enqueuedLineCount;
391
+ await this.until(() => this.writer.settledLineCount + this.writer.droppedLineCount >= mark, "QuestDB has not taken the last rows");
392
+ this.checkDrops();
393
+ }
394
+ async until(done, failure) {
395
+ const deadline = Date.now() + this.timeoutMs;
396
+ while (!done()) {
397
+ this.checkDrops();
398
+ if (Date.now() > deadline) {
399
+ throw new Error(`${failure} after ${Math.round(this.timeoutMs / 1000)} s. Run the ` +
400
+ `import again once QuestDB is healthy: rows already imported are ` +
401
+ `not duplicated.`);
402
+ }
403
+ await new Promise((resolve) => setTimeout(resolve, this.pollMs));
404
+ }
405
+ }
406
+ checkDrops() {
407
+ const dropped = this.writer.droppedLineCount - this.droppedAtStart;
408
+ if (dropped > 0) {
409
+ throw new Error(`The writer dropped ${dropped} lines while QuestDB was behind, so ` +
410
+ `part of the import may be missing. Run it again: rows already ` +
411
+ `imported are not duplicated.`);
412
+ }
413
+ }
414
+ }
415
+ /**
416
+ * The five-minute intervals of a day in which the own vessel has positions
417
+ * that did not come from an import, read from the base table and, where it
418
+ * covers the day, the archive.
419
+ */
420
+ export function recordedIntervals(queryClient, archive) {
421
+ return async (dayStartMs) => {
422
+ const from = new Date(dayStartMs).toISOString();
423
+ const to = new Date(dayStartMs + DAY_MS).toISOString();
424
+ const table = readFrom("signalk_position", archive.covers("signalk_position", from, to));
425
+ const result = await queryClient.exec(`SELECT ts, count() FROM ${table} WHERE ts >= '${from}' AND ts < '${to}' ` +
426
+ `AND context = 'self' AND (source IS NULL OR source != '${IMPORT_SOURCE}') ` +
427
+ `SAMPLE BY 5m ALIGN TO CALENDAR`);
428
+ return new Set(result.dataset.map((row) => Math.floor(Date.parse(String(row[0])) / COVERAGE_MS)));
429
+ };
430
+ }
431
+ /**
432
+ * Wait until the imported row stamped `ms` reads back from the archive.
433
+ * Readable means QuestDB committed and applied it, and with it every row the
434
+ * writer sent before.
435
+ */
436
+ export async function waitForImportedRow(queryClient, ms, timeoutMs, pollMs = 1000) {
437
+ const deadline = Date.now() + timeoutMs;
438
+ for (;;) {
439
+ const result = await queryClient.exec(`SELECT count() FROM ${ARCHIVE_TABLES.signalk_position} ` +
440
+ `WHERE ts = '${new Date(ms).toISOString()}' AND context = 'self' ` +
441
+ `AND source = '${IMPORT_SOURCE}'`);
442
+ if (Number(result.dataset[0]?.[0] ?? 0) > 0)
443
+ return;
444
+ if (Date.now() > deadline) {
445
+ throw new Error("QuestDB has not stored the last imported row. Run the import " +
446
+ "again once QuestDB is healthy: rows already imported are not " +
447
+ "duplicated.");
448
+ }
449
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
450
+ }
451
+ }
452
+ //# sourceMappingURL=gpx-import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpx-import.js","sourceRoot":"","sources":["../src/gpx-import.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { QueryClient } from "./query-client.js";
2
+ import { type ArchiveReader } from "./archive.js";
2
3
  interface HistoryOptions {
3
4
  startTime: Date;
4
5
  playbackRate: number;
@@ -15,7 +16,7 @@ interface Delta {
15
16
  }[];
16
17
  }[];
17
18
  }
18
- export declare function createHistoryProviderV1(queryClient: QueryClient, selfContext: string, debug: (msg: string) => void): {
19
+ export declare function createHistoryProviderV1(queryClient: QueryClient, selfContext: string, debug: (msg: string) => void, archive?: ArchiveReader): {
19
20
  hasAnyData: (options: HistoryOptions, callback: (hasResults: boolean) => void) => void;
20
21
  streamHistory: (spark: {
21
22
  write: (data: unknown) => void;
@@ -1,5 +1,6 @@
1
1
  import { validateTimestamp } from "./query-client.js";
2
2
  import { isMissingKindColumn, isMissingSourceColumn } from "./schema-errors.js";
3
+ import { ARCHIVE_TABLES, NO_ARCHIVE } from "./archive.js";
3
4
  // v1 replays raw deltas rather than aggregating, so all three value tables can
4
5
  // be read in one pass and interleaved by timestamp. Every branch below emits
5
6
  // the same unified shape — ts, path, context, valuetext, kind — which
@@ -18,10 +19,10 @@ import { isMissingKindColumn, isMissingSourceColumn } from "./schema-errors.js";
18
19
  function sourceCol(withSource) {
19
20
  return withSource ? `CAST(source AS STRING)` : `CAST(NULL AS STRING)`;
20
21
  }
21
- function numBranch(where, withSource, tail = "") {
22
+ function numBranch(where, withSource, tail = "", table = "signalk") {
22
23
  return (`SELECT ts, path, context, ${sourceCol(withSource)} source, ` +
23
24
  `CAST(value AS STRING) valuetext, ` +
24
- `'number' kind FROM signalk WHERE ${where}${tail}`);
25
+ `'number' kind FROM ${table} WHERE ${where}${tail}`);
25
26
  }
26
27
  // `withKind: false` degrades to the pre-migration shape. ensureTables() adds
27
28
  // value_kind at startup, but a read racing an unmigrated table (or an
@@ -36,16 +37,41 @@ function strBranch(where, withKind, withSource, tail = "") {
36
37
  `FROM signalk_str WHERE ${where}${tail}`);
37
38
  }
38
39
  // The track table has no path column, so the path is supplied as a literal.
39
- function posBranch(where, withSource, tail = "") {
40
+ function posBranch(where, withSource, tail = "", table = "signalk_position") {
40
41
  return (`SELECT ts, 'navigation.position' path, context, ` +
41
42
  `${sourceCol(withSource)} source, ` +
42
43
  `concat(CAST(lat AS STRING), ',', CAST(lon AS STRING)) valuetext, ` +
43
- `'position' kind FROM signalk_position WHERE ${where}${tail}`);
44
+ `'position' kind FROM ${table} WHERE ${where}${tail}`);
44
45
  }
45
- function unionValueRowsSQL(where, orderAndLimit, withKind = true, withSource = true) {
46
- return (`${numBranch(where, withSource)} UNION ALL ` +
47
- `${strBranch(where, withKind, withSource)} UNION ALL ` +
48
- `${posBranch(where, withSource)} ${orderAndLimit}`);
46
+ const NOT_ARCHIVED = { numeric: false, position: false };
47
+ function unionValueRowsSQL(where, orderAndLimit, withKind = true, withSource = true, archived = NOT_ARCHIVED) {
48
+ const branches = [
49
+ numBranch(where, withSource),
50
+ strBranch(where, withKind, withSource),
51
+ posBranch(where, withSource),
52
+ ];
53
+ if (archived.numeric) {
54
+ branches.push(numBranch(where, withSource, "", ARCHIVE_TABLES.signalk));
55
+ }
56
+ if (archived.position) {
57
+ branches.push(posBranch(where, withSource, "", ARCHIVE_TABLES.signalk_position));
58
+ }
59
+ return `${branches.join(" UNION ALL ")} ${orderAndLimit}`;
60
+ }
61
+ /**
62
+ * Keep the newest row per path and context. A snapshot reading the archive
63
+ * as well as the base tables gets a LATEST ON row from each for a path both
64
+ * hold, and the older one must not end up applied after the newer.
65
+ */
66
+ function newestPerPath(rows) {
67
+ const newest = new Map();
68
+ for (const row of rows) {
69
+ const key = `${String(row.path)}\u0000${String(row.context)}`;
70
+ const held = newest.get(key);
71
+ if (!held || String(row.ts) > String(held.ts))
72
+ newest.set(key, row);
73
+ }
74
+ return [...newest.values()];
49
75
  }
50
76
  // Turn a unified row back into the value a Signal K delta carries. Numbers
51
77
  // must not stay strings (consumers do arithmetic on them), positions must be
@@ -159,7 +185,16 @@ async function fetchLatestNames(queryClient, startTime) {
159
185
  return new Map();
160
186
  }
161
187
  }
162
- export function createHistoryProviderV1(queryClient, selfContext, debug) {
188
+ export function createHistoryProviderV1(queryClient, selfContext, debug,
189
+ // Where retention kept the own vessel's older rows. A read includes it only
190
+ // for a range it covers.
191
+ archive = NO_ARCHIVE) {
192
+ function archivedIn(from, to) {
193
+ return {
194
+ numeric: archive.covers("signalk", from, to),
195
+ position: archive.covers("signalk_position", from, to),
196
+ };
197
+ }
163
198
  // Run a unified read, retrying without whichever optional column QuestDB
164
199
  // reports missing. value_kind and source are separate migrations, so
165
200
  // either — or both, on an external database ensureTables() has not
@@ -206,14 +241,21 @@ export function createHistoryProviderV1(queryClient, selfContext, debug) {
206
241
  // "Any data" must consider every value table: a vessel recording only
207
242
  // string/boolean channels (or only a position track) has history to play
208
243
  // back, and answering false here disables playback entirely.
244
+ const tables = ["signalk", "signalk_str", "signalk_position"];
245
+ const archived = archivedIn(startTime, new Date().toISOString());
246
+ if (archived.numeric)
247
+ tables.push(ARCHIVE_TABLES.signalk);
248
+ if (archived.position)
249
+ tables.push(ARCHIVE_TABLES.signalk_position);
209
250
  queryClient
210
251
  .exec(
211
252
  // Summed via a UNION ALL subquery: QuestDB rejects adding scalar
212
253
  // subqueries with `+` ("no matching operator").
213
254
  `SELECT sum(c) as cnt FROM (` +
214
- `SELECT count() c FROM signalk WHERE ts >= '${startTime}'` +
215
- ` UNION ALL SELECT count() c FROM signalk_str WHERE ts >= '${startTime}'` +
216
- ` UNION ALL SELECT count() c FROM signalk_position WHERE ts >= '${startTime}')`)
255
+ tables
256
+ .map((table) => `SELECT count() c FROM ${table} WHERE ts >= '${startTime}'`)
257
+ .join(" UNION ALL ") +
258
+ `)`)
217
259
  .then((result) => {
218
260
  const count = result.dataset.length > 0 ? result.dataset[0][0] : 0;
219
261
  callback(count > 0);
@@ -270,7 +312,8 @@ export function createHistoryProviderV1(queryClient, selfContext, debug) {
270
312
  try {
271
313
  const window = `ts >= '${from}' AND ts < '${to}'`;
272
314
  const order = `ORDER BY ts LIMIT ${CHUNK_ROW_LIMIT}`;
273
- const result = await execUnified((withKind, withSource) => unionValueRowsSQL(window, order, withKind, withSource), columnState);
315
+ const archived = archivedIn(from, to);
316
+ const result = await execUnified((withKind, withSource) => unionValueRowsSQL(window, order, withKind, withSource, archived), columnState);
274
317
  if (result.dataset.length === 0) {
275
318
  currentTime = chunkEnd;
276
319
  if (!stopped) {
@@ -385,14 +428,29 @@ export function createHistoryProviderV1(queryClient, selfContext, debug) {
385
428
  const at = `ts <= '${ts}'`;
386
429
  const byPath = ` LATEST ON ts PARTITION BY path, context`;
387
430
  const byContext = ` LATEST ON ts PARTITION BY context`;
388
- const snapshotSQL = (withKind, withSource) => `(${numBranch(at, withSource, byPath)})` +
389
- ` UNION ALL ` +
390
- `(${strBranch(at, withKind, withSource, byPath)})` +
391
- ` UNION ALL ` +
392
- `(${posBranch(at, withSource, byContext)})`;
431
+ // The archive answers for paths whose base rows retention has dropped,
432
+ // as one more per-table LATEST ON branch, never by wrapping the union.
433
+ const archived = archivedIn(new Date(0).toISOString(), ts);
434
+ const snapshotSQL = (withKind, withSource) => {
435
+ const branches = [
436
+ numBranch(at, withSource, byPath),
437
+ strBranch(at, withKind, withSource, byPath),
438
+ posBranch(at, withSource, byContext),
439
+ ];
440
+ if (archived.numeric) {
441
+ branches.push(numBranch(at, withSource, byPath, ARCHIVE_TABLES.signalk));
442
+ }
443
+ if (archived.position) {
444
+ branches.push(posBranch(at, withSource, byContext, ARCHIVE_TABLES.signalk_position));
445
+ }
446
+ return branches.map((branch) => `(${branch})`).join(" UNION ALL ");
447
+ };
393
448
  execUnified(snapshotSQL)
394
449
  .then((result) => {
395
- const rows = queryClient.toObjects(result);
450
+ const objects = queryClient.toObjects(result);
451
+ const rows = archived.numeric || archived.position
452
+ ? newestPerPath(objects)
453
+ : objects;
396
454
  const deltas = groupRowsIntoDeltas(rows);
397
455
  callback(deltas);
398
456
  })