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.
Files changed (47) hide show
  1. package/.coderabbit.yaml +19 -15
  2. package/.release-please-manifest.json +1 -1
  3. package/CLAUDE.md +6 -3
  4. package/README.md +102 -7
  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/gpx-import-session.d.ts +69 -0
  13. package/dist/gpx-import-session.js +218 -0
  14. package/dist/gpx-import-session.js.map +1 -0
  15. package/dist/gpx-import.d.ts +135 -0
  16. package/dist/gpx-import.js +452 -0
  17. package/dist/gpx-import.js.map +1 -0
  18. package/dist/history-v1.d.ts +2 -1
  19. package/dist/history-v1.js +77 -19
  20. package/dist/history-v1.js.map +1 -1
  21. package/dist/history-v2.d.ts +2 -1
  22. package/dist/history-v2.js +131 -65
  23. package/dist/history-v2.js.map +1 -1
  24. package/dist/ilp-writer.d.ts +1 -1
  25. package/dist/ilp-writer.js +4 -2
  26. package/dist/ilp-writer.js.map +1 -1
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.js +239 -10
  29. package/dist/index.js.map +1 -1
  30. package/dist/query-client.js +27 -0
  31. package/dist/query-client.js.map +1 -1
  32. package/dist/retention.d.ts +28 -1
  33. package/dist/retention.js +152 -12
  34. package/dist/retention.js.map +1 -1
  35. package/package.json +2 -1
  36. package/public/assets/PluginConfigurationPanel-DR-y5e3d.js +11 -0
  37. 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
  38. package/public/assets/{dist-BI-Ygd_Z.js → dist-C5M3fhBw.js} +1 -1
  39. package/public/assets/helpers-oIxDNnZJ.js +1 -0
  40. package/public/assets/{hostInit-CdqUOHAJ.js → hostInit-B3lIJd9C.js} +1 -1
  41. package/public/assets/{index-mvK71Kw7.js → index-eAjx9ZuX.js} +2 -2
  42. package/public/assets/{pendingShares-BI6kviiM.js → pendingShares-PvyVL1Z8.js} +2 -2
  43. package/public/assets/utils-v9GZlbdk.js +4 -0
  44. package/public/remoteEntry.js +2 -2
  45. package/public/assets/PluginConfigurationPanel-JR1rvS7f.js +0 -9
  46. package/public/assets/helpers-CsBAxMau.js +0 -1
  47. package/public/assets/utils-BRGuweMc.js +0 -4
@@ -0,0 +1,69 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+ import type { GpxImportStatus } from "./api-contract.js";
3
+ import type { GpxImport } from "./gpx-import.js";
4
+ /** The largest slice one request may carry. */
5
+ export declare const MAX_SLICE_BYTES: number;
6
+ /** An import nobody has sent a slice to for this long is abandoned. */
7
+ export declare const IDLE_TIMEOUT_MS: number;
8
+ /** The request does not fit the import's state; carries where it stands. */
9
+ export declare class GpxImportConflict extends Error {
10
+ readonly status: GpxImportStatus | null;
11
+ constructor(message: string, status: GpxImportStatus | null);
12
+ }
13
+ export declare class SliceTooLarge extends Error {
14
+ }
15
+ export declare class GpxImportSession {
16
+ readonly filename: string;
17
+ readonly size: number;
18
+ private readonly importer;
19
+ /** Resolves once QuestDB has applied everything the import wrote. */
20
+ private readonly applied;
21
+ private readonly now;
22
+ readonly id: `${string}-${string}-${string}-${string}-${string}`;
23
+ private state;
24
+ private received;
25
+ private error;
26
+ private busy;
27
+ private readonly startedAt;
28
+ private updatedAt;
29
+ private lastActivity;
30
+ constructor(filename: string, size: number, importer: GpxImport,
31
+ /** Resolves once QuestDB has applied everything the import wrote. */
32
+ applied: () => Promise<void>, now?: () => number);
33
+ get status(): GpxImportStatus;
34
+ get active(): boolean;
35
+ /** Receiving, but nothing has arrived for IDLE_TIMEOUT_MS. */
36
+ get idle(): boolean;
37
+ append(offset: number, bytes: Uint8Array): Promise<GpxImportStatus>;
38
+ finish(): Promise<GpxImportStatus>;
39
+ /** End the import. What it already wrote stays; dedup absorbs a re-run. */
40
+ abort(reason?: string): void;
41
+ private expectReceiving;
42
+ private fail;
43
+ private touch;
44
+ }
45
+ /**
46
+ * One import at a time: they share the ILP writer, and two progress bars
47
+ * against one writer would tell the user nothing. The last import's status
48
+ * stays readable after it ends, so a panel opened later can show its result.
49
+ */
50
+ export declare class GpxImports {
51
+ private current;
52
+ start(create: () => GpxImportSession): GpxImportSession;
53
+ get(id: string): GpxImportSession | null;
54
+ latest(): GpxImportStatus | null;
55
+ /** An import holds the writer: receiving, or finishing what it received. */
56
+ get active(): boolean;
57
+ /** The plugin is stopping: the writer the import uses is going away. */
58
+ abortAll(): void;
59
+ private reapIdle;
60
+ }
61
+ /**
62
+ * The body of a slice request, refused past `limit` bytes. Read here rather
63
+ * than by a body parser: signalk-server parses JSON and form bodies only, so
64
+ * an octet-stream arrives untouched.
65
+ */
66
+ export declare function readSlice(req: NodeJS.ReadableStream & {
67
+ headers: IncomingHttpHeaders;
68
+ readableEnded?: boolean;
69
+ }, limit?: number): Promise<Buffer>;
@@ -0,0 +1,218 @@
1
+ // The upload side of a GPX import: the file arrives as numbered slices over
2
+ // several requests, because one request carrying 1.5 GB outlasts Node's
3
+ // five-minute request timeout on a boat's Wi-Fi and is refused by any proxy
4
+ // with a body limit. Each slice names the byte offset it starts at, so a
5
+ // client that lost a reply can ask where to resume instead of guessing, and a
6
+ // slice sent twice is refused rather than imported twice.
7
+ //
8
+ // A slice's reply waits until its points are with the writer and the writer
9
+ // has room again. That is the whole flow control: the browser sends the next
10
+ // slice only when the server is ready for it.
11
+ import { randomUUID } from "node:crypto";
12
+ /** The largest slice one request may carry. */
13
+ export const MAX_SLICE_BYTES = 8 * 1024 * 1024;
14
+ /** An import nobody has sent a slice to for this long is abandoned. */
15
+ export const IDLE_TIMEOUT_MS = 15 * 60 * 1000;
16
+ /** The request does not fit the import's state; carries where it stands. */
17
+ export class GpxImportConflict extends Error {
18
+ status;
19
+ constructor(message, status) {
20
+ super(message);
21
+ this.status = status;
22
+ }
23
+ }
24
+ export class SliceTooLarge extends Error {
25
+ }
26
+ export class GpxImportSession {
27
+ filename;
28
+ size;
29
+ importer;
30
+ applied;
31
+ now;
32
+ id = randomUUID();
33
+ state = "receiving";
34
+ received = 0;
35
+ error;
36
+ busy = false;
37
+ startedAt;
38
+ updatedAt;
39
+ lastActivity;
40
+ constructor(filename, size, importer,
41
+ /** Resolves once QuestDB has applied everything the import wrote. */
42
+ applied, now = Date.now) {
43
+ this.filename = filename;
44
+ this.size = size;
45
+ this.importer = importer;
46
+ this.applied = applied;
47
+ this.now = now;
48
+ this.lastActivity = now();
49
+ this.startedAt = new Date(this.lastActivity).toISOString();
50
+ this.updatedAt = this.startedAt;
51
+ }
52
+ get status() {
53
+ return {
54
+ id: this.id,
55
+ filename: this.filename,
56
+ size: this.size,
57
+ received: this.received,
58
+ state: this.state,
59
+ summary: { ...this.importer.summary },
60
+ ...(this.error !== undefined ? { error: this.error } : {}),
61
+ startedAt: this.startedAt,
62
+ updatedAt: this.updatedAt,
63
+ };
64
+ }
65
+ get active() {
66
+ return this.state === "receiving" || this.state === "finishing";
67
+ }
68
+ /** Receiving, but nothing has arrived for IDLE_TIMEOUT_MS. */
69
+ get idle() {
70
+ return (this.state === "receiving" &&
71
+ !this.busy &&
72
+ this.now() - this.lastActivity > IDLE_TIMEOUT_MS);
73
+ }
74
+ async append(offset, bytes) {
75
+ this.expectReceiving();
76
+ if (offset !== this.received) {
77
+ throw new GpxImportConflict(`The next slice starts at byte ${this.received}, not ${offset}`, this.status);
78
+ }
79
+ if (this.received + bytes.length > this.size) {
80
+ throw new GpxImportConflict(`The slice runs past the announced size of ${this.size} bytes`, this.status);
81
+ }
82
+ this.busy = true;
83
+ try {
84
+ await this.importer.write(bytes);
85
+ this.received += bytes.length;
86
+ }
87
+ catch (err) {
88
+ this.fail(err);
89
+ throw err;
90
+ }
91
+ finally {
92
+ this.busy = false;
93
+ this.touch();
94
+ }
95
+ return this.status;
96
+ }
97
+ async finish() {
98
+ this.expectReceiving();
99
+ if (this.received !== this.size) {
100
+ throw new GpxImportConflict(`Only ${this.received} of ${this.size} bytes have arrived`, this.status);
101
+ }
102
+ this.state = "finishing";
103
+ this.busy = true;
104
+ this.touch();
105
+ try {
106
+ await this.importer.finish();
107
+ await this.applied();
108
+ // An abort while finishing has the last word.
109
+ if (this.state === "finishing")
110
+ this.state = "finished";
111
+ }
112
+ catch (err) {
113
+ this.fail(err);
114
+ throw err;
115
+ }
116
+ finally {
117
+ this.busy = false;
118
+ this.touch();
119
+ }
120
+ return this.status;
121
+ }
122
+ /** End the import. What it already wrote stays; dedup absorbs a re-run. */
123
+ abort(reason = "Aborted") {
124
+ if (!this.active)
125
+ return;
126
+ this.importer.cancel();
127
+ this.state = "aborted";
128
+ this.error = reason;
129
+ this.touch();
130
+ }
131
+ expectReceiving() {
132
+ if (this.state !== "receiving") {
133
+ throw new GpxImportConflict(`The import is ${this.state}`, this.status);
134
+ }
135
+ if (this.busy) {
136
+ throw new GpxImportConflict("The previous slice is still being imported", this.status);
137
+ }
138
+ }
139
+ fail(err) {
140
+ // An abort already set the final state and its reason.
141
+ if (this.state === "aborted")
142
+ return;
143
+ this.state = "failed";
144
+ this.error = err instanceof Error ? err.message : String(err);
145
+ }
146
+ touch() {
147
+ this.lastActivity = this.now();
148
+ this.updatedAt = new Date(this.lastActivity).toISOString();
149
+ }
150
+ }
151
+ /**
152
+ * One import at a time: they share the ILP writer, and two progress bars
153
+ * against one writer would tell the user nothing. The last import's status
154
+ * stays readable after it ends, so a panel opened later can show its result.
155
+ */
156
+ export class GpxImports {
157
+ current = null;
158
+ start(create) {
159
+ this.reapIdle();
160
+ if (this.current?.active) {
161
+ throw new GpxImportConflict("A GPX import is already running", this.current.status);
162
+ }
163
+ this.current = create();
164
+ return this.current;
165
+ }
166
+ get(id) {
167
+ this.reapIdle();
168
+ return this.current?.id === id ? this.current : null;
169
+ }
170
+ latest() {
171
+ this.reapIdle();
172
+ return this.current?.status ?? null;
173
+ }
174
+ /** An import holds the writer: receiving, or finishing what it received. */
175
+ get active() {
176
+ this.reapIdle();
177
+ return this.current?.active ?? false;
178
+ }
179
+ /** The plugin is stopping: the writer the import uses is going away. */
180
+ abortAll() {
181
+ this.current?.abort("The plugin stopped");
182
+ }
183
+ reapIdle() {
184
+ if (this.current?.idle) {
185
+ this.current.abort(`No slice arrived for ${IDLE_TIMEOUT_MS / 60000} minutes`);
186
+ }
187
+ }
188
+ }
189
+ /**
190
+ * The body of a slice request, refused past `limit` bytes. Read here rather
191
+ * than by a body parser: signalk-server parses JSON and form bodies only, so
192
+ * an octet-stream arrives untouched.
193
+ */
194
+ export function readSlice(req, limit = MAX_SLICE_BYTES) {
195
+ if (Number(req.headers["content-length"]) > limit) {
196
+ return Promise.reject(new SliceTooLarge(`A slice is at most ${limit} bytes`));
197
+ }
198
+ if (req.readableEnded) {
199
+ return Promise.reject(new Error("The request body was already read"));
200
+ }
201
+ return new Promise((resolve, reject) => {
202
+ const chunks = [];
203
+ let size = 0;
204
+ req.on("data", (chunk) => {
205
+ size += chunk.length;
206
+ if (size > limit) {
207
+ reject(new SliceTooLarge(`A slice is at most ${limit} bytes`));
208
+ req.removeAllListeners("data");
209
+ req.resume();
210
+ return;
211
+ }
212
+ chunks.push(chunk);
213
+ });
214
+ req.on("end", () => resolve(Buffer.concat(chunks)));
215
+ req.on("error", reject);
216
+ });
217
+ }
218
+ //# sourceMappingURL=gpx-import-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpx-import-session.js","sourceRoot":"","sources":["../src/gpx-import-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import type { QueryClient } from "./query-client.js";
2
+ import type { ILPWriter } from "./ilp-writer.js";
3
+ import type { GpxImportSummary } from "./api-contract.js";
4
+ import { type ArchiveReader } from "./archive.js";
5
+ /** The sourceRef imported positions are stored under. */
6
+ export declare const IMPORT_SOURCE = "import.gpx";
7
+ /**
8
+ * An attribute as an `xsd:decimal`, or NaN. `Number()` reads `0x10` and
9
+ * `1e1` too, which GPX does not allow and no exporter means.
10
+ */
11
+ export declare function gpxDecimal(value: string | undefined): number;
12
+ /**
13
+ * A `<time>` as epoch milliseconds, or NaN. GPX types it `xsd:dateTime`;
14
+ * `Date.parse` alone takes a bare year and rolls February 30th into March.
15
+ */
16
+ export declare function gpxDateTime(value: string): number;
17
+ /**
18
+ * A longitude brought into [-180, 180], or NaN when it is not one.
19
+ *
20
+ * Crossing the antimeridian, TimeZero writes the fixes just past it as
21
+ * -180.00002 rather than 179.99998. Those are real positions, so they are
22
+ * wrapped; a value beyond a full turn is not a position at all.
23
+ */
24
+ export declare function antimeridian(longitude: number): number;
25
+ export interface GpxImportOptions {
26
+ /**
27
+ * Names of the tracks to import, "" for a track without one.
28
+ *
29
+ * GPX has no field that says whose a track is, and a chartplotter's export
30
+ * mixes the own track with AIS targets and with passages imported from
31
+ * elsewhere — a TimeZero export put another boat's Fiji passage on the same
32
+ * day as the own track in Polynesia. Written into one series, those draw a
33
+ * line across the ocean, so the caller says which tracks are the vessel's.
34
+ */
35
+ tracks: ReadonlySet<string>;
36
+ /** One row per this many seconds; 0 keeps every point's own time. */
37
+ resolutionSeconds: number;
38
+ /** Write one own-vessel position, stamped `ms`. */
39
+ write(ms: number, latitude: number, longitude: number): void;
40
+ /** Resolves once the writer can take more; rejects if it never will. */
41
+ backpressure(): Promise<void>;
42
+ /**
43
+ * The five-minute intervals of a UTC day (by `floor(ms / 5 min)`) in which
44
+ * the vessel already has positions. Absent, every point is imported.
45
+ */
46
+ recorded?: (dayStartMs: number) => Promise<ReadonlySet<number>>;
47
+ }
48
+ /** The document is not well-formed XML; rows before the error are kept. */
49
+ export declare class GpxParseError extends Error {
50
+ }
51
+ /** The import was aborted while a slice was being written. */
52
+ export declare class GpxImportCancelled extends Error {
53
+ }
54
+ /**
55
+ * One import, fed the document slice by slice.
56
+ *
57
+ * Only GPX's own structure counts: a `trk` under `gpx`, a `trkseg` under it,
58
+ * a `trkpt` under that. A vendor `<trkseg>` inside `<extensions>` is not a
59
+ * segment, and a `<name>` inside a point is not the track's name.
60
+ */
61
+ export declare class GpxImport {
62
+ private readonly options;
63
+ readonly summary: GpxImportSummary;
64
+ private readonly parser;
65
+ private readonly decoder;
66
+ private parseError;
67
+ private cancelled;
68
+ private readonly path;
69
+ private trackName;
70
+ private nameText;
71
+ private selected;
72
+ private segment;
73
+ private point;
74
+ private timeText;
75
+ private pending;
76
+ private lastSegment;
77
+ private last;
78
+ private lastBucket;
79
+ private readonly coverage;
80
+ private coveredDay;
81
+ private covered;
82
+ constructor(options: GpxImportOptions);
83
+ /**
84
+ * The instant of the row written last, or null before the first. Not
85
+ * `summary.to`: an export out of time order writes its latest row early.
86
+ */
87
+ get lastWritten(): number | null;
88
+ /** Stop writing: the slice in progress ends at its next point. */
89
+ cancel(): void;
90
+ /** Parse the next bytes of the document and write the points they hold. */
91
+ write(chunk: Uint8Array): Promise<void>;
92
+ /** The document has ended: anything left unclosed is an error. */
93
+ finish(): Promise<GpxImportSummary>;
94
+ private parse;
95
+ private throwParseError;
96
+ private at;
97
+ private open;
98
+ private text;
99
+ private close;
100
+ private name;
101
+ private recordedIn;
102
+ private drain;
103
+ }
104
+ type ImportWriter = Pick<ILPWriter, "pendingLines" | "enqueuedLineCount" | "settledLineCount" | "droppedLineCount">;
105
+ /**
106
+ * What an import needs from the shared ILP writer: to wait while it is full,
107
+ * to know when everything written has left it, and to notice when it had to
108
+ * drop lines, which may have been the import's.
109
+ */
110
+ export declare class WriterGate {
111
+ private readonly writer;
112
+ private readonly pollMs;
113
+ private readonly timeoutMs;
114
+ private readonly droppedAtStart;
115
+ constructor(writer: ImportWriter, pollMs?: number, timeoutMs?: number);
116
+ /** Resolves once the writer has room; rejects if it does not drain. */
117
+ room(): Promise<void>;
118
+ /** Resolves once every line enqueued so far has left the writer. */
119
+ settled(): Promise<void>;
120
+ private until;
121
+ private checkDrops;
122
+ }
123
+ /**
124
+ * The five-minute intervals of a day in which the own vessel has positions
125
+ * that did not come from an import, read from the base table and, where it
126
+ * covers the day, the archive.
127
+ */
128
+ export declare function recordedIntervals(queryClient: Pick<QueryClient, "exec">, archive: ArchiveReader): (dayStartMs: number) => Promise<ReadonlySet<number>>;
129
+ /**
130
+ * Wait until the imported row stamped `ms` reads back from the archive.
131
+ * Readable means QuestDB committed and applied it, and with it every row the
132
+ * writer sent before.
133
+ */
134
+ export declare function waitForImportedRow(queryClient: Pick<QueryClient, "exec">, ms: number, timeoutMs: number, pollMs?: number): Promise<void>;
135
+ export {};