signalk-questdb 2.2.3 → 2.3.1
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/.coderabbit.yaml +19 -15
- package/.release-please-manifest.json +1 -1
- package/CLAUDE.md +6 -3
- package/README.md +102 -7
- package/dist/api-contract.d.ts +46 -0
- package/dist/archive.d.ts +44 -0
- package/dist/archive.js +117 -0
- package/dist/archive.js.map +1 -0
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.js +27 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/gpx-import-session.d.ts +69 -0
- package/dist/gpx-import-session.js +218 -0
- package/dist/gpx-import-session.js.map +1 -0
- package/dist/gpx-import.d.ts +135 -0
- package/dist/gpx-import.js +452 -0
- package/dist/gpx-import.js.map +1 -0
- package/dist/history-v1.d.ts +2 -1
- package/dist/history-v1.js +77 -19
- package/dist/history-v1.js.map +1 -1
- package/dist/history-v2.d.ts +2 -1
- package/dist/history-v2.js +131 -65
- package/dist/history-v2.js.map +1 -1
- package/dist/ilp-writer.d.ts +1 -1
- package/dist/ilp-writer.js +4 -2
- package/dist/ilp-writer.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +239 -10
- package/dist/index.js.map +1 -1
- package/dist/query-client.js +27 -0
- package/dist/query-client.js.map +1 -1
- package/dist/retention.d.ts +28 -1
- package/dist/retention.js +152 -12
- package/dist/retention.js.map +1 -1
- package/package.json +2 -1
- package/public/assets/PluginConfigurationPanel-DR-y5e3d.js +11 -0
- 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
- package/public/assets/{dist-BI-Ygd_Z.js → dist-C5M3fhBw.js} +1 -1
- package/public/assets/helpers-oIxDNnZJ.js +1 -0
- package/public/assets/{hostInit-CdqUOHAJ.js → hostInit-B3lIJd9C.js} +1 -1
- package/public/assets/{index-mvK71Kw7.js → index-eAjx9ZuX.js} +2 -2
- package/public/assets/{pendingShares-BI6kviiM.js → pendingShares-PvyVL1Z8.js} +2 -2
- package/public/assets/utils-v9GZlbdk.js +4 -0
- package/public/remoteEntry.js +2 -2
- package/public/assets/PluginConfigurationPanel-JR1rvS7f.js +0 -9
- package/public/assets/helpers-CsBAxMau.js +0 -1
- 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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"}
|
package/dist/history-v1.d.ts
CHANGED
|
@@ -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;
|
package/dist/history-v1.js
CHANGED
|
@@ -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
|
|
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
|
|
44
|
+
`'position' kind FROM ${table} WHERE ${where}${tail}`);
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
})
|