signalk-questdb 2.2.3 → 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.
- package/.coderabbit.yaml +19 -15
- package/.release-please-manifest.json +1 -1
- package/CLAUDE.md +4 -3
- package/README.md +94 -5
- 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 +54 -26
- 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
package/.coderabbit.yaml
CHANGED
|
@@ -51,10 +51,12 @@ reviews:
|
|
|
51
51
|
|
|
52
52
|
1. **Plugin runtime** (`src/`, TypeScript, compiled to `dist/`).
|
|
53
53
|
Loaded by signalk-server as `main` (`dist/index.js`).
|
|
54
|
-
2. **React config panel** (`src/configpanel/`,
|
|
55
|
-
|
|
54
|
+
2. **React config panel** (`src/configpanel/`, TypeScript + JSX).
|
|
55
|
+
Bundled by Vite via Module Federation into
|
|
56
56
|
`public/remoteEntry.js`. React 19 is a shared singleton with
|
|
57
|
-
the host Admin UI.
|
|
57
|
+
the host Admin UI. Imports leaving `src/configpanel/` must be
|
|
58
|
+
`import type`: a value import pulls server-side runtime code
|
|
59
|
+
(typebox, `fs/promises`, saxes) into the browser bundle.
|
|
58
60
|
|
|
59
61
|
Both build outputs ship in the npm package; `prepublishOnly`
|
|
60
62
|
rebuilds them.
|
|
@@ -95,17 +97,17 @@ reviews:
|
|
|
95
97
|
|
|
96
98
|
## Config schema is the source of truth
|
|
97
99
|
|
|
98
|
-
`src/config/schema.ts` uses TypeBox (
|
|
100
|
+
`src/config/schema.ts` uses TypeBox (the unscoped `typebox` 1.x). The
|
|
99
101
|
schema is BOTH the JSON Schema rendered by the Signal K Admin UI
|
|
100
102
|
AND the source of the `Config` TypeScript type via `Static<...>`.
|
|
101
103
|
Add new options there only.
|
|
102
104
|
|
|
103
|
-
- The
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`default:` value and the corresponding `useState
|
|
108
|
-
|
|
105
|
+
- The configpanel imports `Config` from `schema.ts` as a type
|
|
106
|
+
only, so it cannot read the schema's defaults. The panel owns
|
|
107
|
+
its own UI state defaults that must be kept in sync with the
|
|
108
|
+
schema defaults. Flag any divergence between the schema's
|
|
109
|
+
`default:` value and the fallback the corresponding `useState`
|
|
110
|
+
in `PluginConfigurationPanel.tsx` gives a missing `cfg.X`.
|
|
109
111
|
- For runtime fallbacks (e.g. `config.foo ?? 1000` in
|
|
110
112
|
`src/index.ts`), use the same literal as the schema default for
|
|
111
113
|
the same reason: schema is canonical, fallback is a backstop
|
|
@@ -132,8 +134,8 @@ reviews:
|
|
|
132
134
|
- `tsconfig.json` runs strict mode. Do not loosen `strict`,
|
|
133
135
|
`noImplicitAny`, `noUnusedLocals`, `noUnusedParameters`. Code
|
|
134
136
|
must narrow `unknown` and avoid `any`.
|
|
135
|
-
- The configpanel is
|
|
136
|
-
|
|
137
|
+
- The configpanel is TypeScript too, with its own `tsconfig.json`
|
|
138
|
+
(browser libs, `noEmit`), checked by `npm run typecheck:panel`.
|
|
137
139
|
- Node `>= 22` per `engines` (uses Node's built-in test runner).
|
|
138
140
|
|
|
139
141
|
## Tests
|
|
@@ -205,9 +207,11 @@ reviews:
|
|
|
205
207
|
— loose coupling is required.
|
|
206
208
|
- `signalk.requires: ['signalk-container']` without a matching
|
|
207
209
|
npm dependency — see "Container integration" above.
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
- Classic JSX in the configpanel (`jsxRuntime: "classic"` in
|
|
211
|
+
`vite.config.ts`, `"jsx": "react"` in its tsconfig). The
|
|
212
|
+
automatic runtime imports `react/jsx-runtime`, which is not in
|
|
213
|
+
the federation share scope, and a second React instance fails
|
|
214
|
+
to mount the panel.
|
|
211
215
|
- The single-worker QuestDB env vars
|
|
212
216
|
(`QDB_SHARED_WORKER_COUNT=1`, etc.) in `src/index.ts` — tuned
|
|
213
217
|
for Pi/Cerbo, do not "fix" by raising.
|
package/CLAUDE.md
CHANGED
|
@@ -31,11 +31,12 @@ Both build outputs are shipped in the npm package; `prepublishOnly` rebuilds the
|
|
|
31
31
|
### Data flow
|
|
32
32
|
|
|
33
33
|
- Subscribes to the Signal K **streambundle** for all paths and writes filtered/throttled deltas to QuestDB via **ILP over raw TCP** (`src/ilp-writer.ts`) — no QuestDB client library.
|
|
34
|
-
- Three tables (`signalk`, `signalk_str`, `signalk_position`) defined and queried in `src/query-client.ts`. All use WAL mode + daily partitioning + dedup.
|
|
34
|
+
- Three tables (`signalk`, `signalk_str`, `signalk_position`) defined and queried in `src/query-client.ts`. All use WAL mode + daily partitioning + dedup. The archives `signalk_archive` and `signalk_position_archive` are defined there too, with their base tables' columns and dedup keys and monthly partitions.
|
|
35
35
|
- Reads come from two registered providers:
|
|
36
36
|
- **v2** (`src/history-v2.ts`) — `app.registerHistoryApiProvider()`, REST under `/signalk/v2/api/history/`. Aggregates map directly to QuestDB SQL except `sma`/`ema` (computed client-side).
|
|
37
37
|
- **v1** (`src/history-v1.ts`) — `app.registerHistoryProvider()`, WebSocket playback with chunked reads.
|
|
38
|
-
- Retention (`src/retention.ts`) drops old daily partitions on a timer.
|
|
38
|
+
- Retention (`src/retention.ts`) drops old daily partitions on a timer. Before a partition of `signalk_position` or `signalk` goes, the own vessel's rows the config keeps are copied, thinned, into `signalk_position_archive` / `signalk_archive` (`src/archive.ts`), and the drop waits for that copy to be applied. Reads add an archive table only for a range it covers (`ArchiveSpans`), so a recent window stays on the base table alone; v1 snapshots read it as one more per-table `LATEST ON` branch, never by wrapping the union.
|
|
39
|
+
- GPX import (`src/gpx-import.ts`, `src/gpx-import-session.ts`) streams a sliced upload through saxes into `signalk_position_archive` under source `import.gpx`, thinned by stamping points on interval starts and letting dedup keep one per interval. The caller names the tracks to import: an export mixes the own track with AIS targets and other boats' passages, and nothing in GPX tells them apart.
|
|
39
40
|
- Startup restore (`src/restore.ts`) optionally replays each vessel's last recorded position back into the live model after a server restart.
|
|
40
41
|
- Console proxy (`src/console-proxy.ts`) reverse-proxies QuestDB's own web console so the embeddable webapp (`public/index.html`) can host it in the admin UI.
|
|
41
42
|
|
|
@@ -72,5 +73,5 @@ Their wire shapes live in `src/api-contract.ts` and are shared by both surfaces:
|
|
|
72
73
|
- Prettier + eslint flat config (`eslint.config.ts`); run `npm run format` before committing.
|
|
73
74
|
- The config panel uses React 19 with Module Federation — keep the federation `shared` block in sync with `package.json`'s React version.
|
|
74
75
|
- **Classic JSX is load-bearing.** `jsxRuntime: "classic"` (vite.config.ts) and `"jsx": "react"` (panel tsconfig) must agree. The automatic runtime imports `react/jsx-runtime`, which is not in the federation `shared` scope, so the remote would carry its own copy of React's jsx runtime — a second React instance whose dispatcher is not the host's, so `useState` reads null and the panel fails to mount. At runtime, with no build error.
|
|
75
|
-
- **Imports leaving `src/configpanel/` must be `import type`.** A value import pulls the module's runtime dependencies into the browser bundle (typebox via `config/schema`, `fs/promises` via `host-limits`). After changing panel imports, confirm with `grep -rl "typebox\|fs/promises" public/` — it must print nothing.
|
|
76
|
+
- **Imports leaving `src/configpanel/` must be `import type`.** A value import pulls the module's runtime dependencies into the browser bundle (typebox via `config/schema`, `fs/promises` via `host-limits`, saxes via `gpx-import`). After changing panel imports, confirm with `grep -rl "typebox\|fs/promises\|saxes" public/` — it must print nothing.
|
|
76
77
|
- `signalk.appIcon` and `signalk.displayName` in `package.json` control how the plugin appears in the Admin UI.
|
package/README.md
CHANGED
|
@@ -20,6 +20,12 @@ InfluxDB history can be imported.
|
|
|
20
20
|
- **Recording controls** — per-path sampling rates, include/exclude filters with
|
|
21
21
|
globs, own vessel and AIS targets separately, retention by days, LZ4/ZSTD
|
|
22
22
|
on-disk compression
|
|
23
|
+
- **Archive past retention** — retention drops old days, but the own track, and
|
|
24
|
+
any own-vessel paths you name such as depth, are kept, thinned, in an archive
|
|
25
|
+
the History API reads seamlessly
|
|
26
|
+
- **GPX track import** — a chartplotter's track export of any size goes into
|
|
27
|
+
the archive; the file is read in the browser first, so you pick the own
|
|
28
|
+
vessel's tracks before anything is uploaded
|
|
23
29
|
- **Startup restore** — AIS targets back on the chart right after a server
|
|
24
30
|
restart, in their ship-type colour
|
|
25
31
|
- **WAL watchdog** — a table QuestDB has silently stopped applying is detected,
|
|
@@ -116,6 +122,9 @@ rootless Podman.
|
|
|
116
122
|
| Allow `sourcePolicy=all` | `false` | Let History API callers split a path into one column per recording source (see History API) |
|
|
117
123
|
| QuestDB console webapp | `true` | Serve QuestDB's console inside the Signal K admin UI, admin only (see Console webapp) |
|
|
118
124
|
| Retention (days) | `0` | Drop daily partitions older than this (0 = keep forever) |
|
|
125
|
+
| Keep own track | `true` | Archive the own vessel's positions before retention drops them (see Retention and the archive) |
|
|
126
|
+
| Archived own-vessel paths | _(empty)_ | Glob patterns of own-vessel numeric paths to archive the same way, e.g. `environment.depth.*` |
|
|
127
|
+
| Archive resolution (s) | `10` | Archived rows are thinned to the first sample per interval (0 = every row) |
|
|
119
128
|
| Filter mode | `exclude` | `exclude` the matching paths, or `include` only the matching paths |
|
|
120
129
|
| Path patterns | _(empty)_ | Glob patterns, one per line, e.g. `environment.wind.*`; empty records everything |
|
|
121
130
|
| Compression codec | `lz4` | On-disk WAL segments and Parquet exports: `none`, `lz4` (fast) or `zstd` (smaller) |
|
|
@@ -135,6 +144,10 @@ Three tables, all with WAL mode, daily partitioning, and deduplication:
|
|
|
135
144
|
| `signalk_str` | String values | `ts`, `path` (SYMBOL), `context` (SYMBOL), `source` (SYMBOL), `value_str` (VARCHAR), `value_kind` (SYMBOL) |
|
|
136
145
|
| `signalk_position` | Positions | `ts`, `context` (SYMBOL), `source` (SYMBOL), `lat` (DOUBLE), `lon` (DOUBLE) |
|
|
137
146
|
|
|
147
|
+
What retention keeps of the own vessel lives in two more tables,
|
|
148
|
+
`signalk_archive` and `signalk_position_archive`, with the same columns and
|
|
149
|
+
monthly partitions (see Retention and the archive).
|
|
150
|
+
|
|
138
151
|
The own vessel is stored as context `self`, whatever its identity; other vessels
|
|
139
152
|
keep their `vessels.urn:…` context. The history API translates `self` back to
|
|
140
153
|
`vessels.self`.
|
|
@@ -221,9 +234,33 @@ if QuestDB is unreachable for longer than that covers, the oldest lines are
|
|
|
221
234
|
dropped and counted. A clean Signal K shutdown flushes first; a hard crash loses
|
|
222
235
|
at most one interval.
|
|
223
236
|
|
|
224
|
-
**
|
|
225
|
-
|
|
226
|
-
|
|
237
|
+
**Compression** applies LZ4 or ZSTD to the WAL segments on disk (and to
|
|
238
|
+
Parquet exports).
|
|
239
|
+
|
|
240
|
+
### Retention and the archive
|
|
241
|
+
|
|
242
|
+
Retention drops whole daily partitions once they lie entirely before the
|
|
243
|
+
configured number of days; 0 keeps everything. QuestDB cannot delete single
|
|
244
|
+
rows, so keeping part of a day means copying it out first. Before a day of
|
|
245
|
+
`signalk_position` or `signalk` is dropped, the own vessel's rows that the
|
|
246
|
+
settings ask to keep are copied into an archive table, thinned to one row per
|
|
247
|
+
**Archive resolution** interval, and the day is dropped only once QuestDB has
|
|
248
|
+
applied the copy. A copy that fails leaves the day in place for the next run.
|
|
249
|
+
|
|
250
|
+
| Table | Holds |
|
|
251
|
+
| -------------------------- | --------------------------------------------------------------- |
|
|
252
|
+
| `signalk_position_archive` | Own-vessel positions (**Keep own track**, on by default) |
|
|
253
|
+
| `signalk_archive` | Own-vessel numeric paths matching **Archived own-vessel paths** |
|
|
254
|
+
|
|
255
|
+
Both have the columns and dedup keys of their base table and are partitioned by
|
|
256
|
+
month. Retention never drops from them, other vessels are never archived, and
|
|
257
|
+
strings are not archived. The History API reads the archive together with the
|
|
258
|
+
base tables for any range it covers, so a track or a depth series continues
|
|
259
|
+
past the retention boundary without a seam. An archived row is the first sample
|
|
260
|
+
of its interval and is stamped on the interval's start.
|
|
261
|
+
|
|
262
|
+
Size, measured: a position row takes 32 bytes, so a vessel recorded around the
|
|
263
|
+
clock fills about 100 MB a year at a 10 s resolution, 34 MB at 30 s.
|
|
227
264
|
|
|
228
265
|
## History API
|
|
229
266
|
|
|
@@ -545,6 +582,49 @@ recorded during the rebuild are carried across. Should recording be fast enough
|
|
|
545
582
|
to fill the writer's buffer during those seconds, the samples it could not keep
|
|
546
583
|
are counted and the panel says how many. Run the import again afterwards.
|
|
547
584
|
|
|
585
|
+
## Importing a GPX track
|
|
586
|
+
|
|
587
|
+
A chartplotter's exported track goes into the position archive (see Retention
|
|
588
|
+
and the archive), where retention never drops it, under the source
|
|
589
|
+
`import.gpx`. An export can be gigabytes, a four-year TimeZero export is
|
|
590
|
+
1.5 GB, so the file is sent in slices and parsed as it arrives. Memory stays at
|
|
591
|
+
one slice, whatever the file's size.
|
|
592
|
+
|
|
593
|
+
In the configuration panel, **GPX track import** reads the chosen file in the
|
|
594
|
+
browser first, which takes seconds even for a gigabyte, and lists its tracks by
|
|
595
|
+
name with their points and period. It proposes the own vessel's tracks: the
|
|
596
|
+
largest group whose name is not an AIS target's, and any other name that
|
|
597
|
+
recurs. It flags a group that lay more than a kilometre from a larger one at
|
|
598
|
+
the same time, since that was another vessel. Nothing is uploaded until
|
|
599
|
+
**Import** is pressed; the import then shows its progress and can be aborted.
|
|
600
|
+
|
|
601
|
+
- **Which tracks.** An export mixes the own track with AIS targets and with
|
|
602
|
+
passages imported into the plotter from elsewhere, and GPX does not say whose
|
|
603
|
+
a track is. The import takes the names of the tracks to import (`tracks`);
|
|
604
|
+
every other track is skipped and listed in the summary. In a TimeZero export
|
|
605
|
+
the own track is named `Own Ship`, or the plotter's translation of it, such
|
|
606
|
+
as `Eigenes Schiff`.
|
|
607
|
+
- **Thinning.** Points are thinned to the **Archive resolution**: each is
|
|
608
|
+
stamped on the start of its interval, and the archive keeps one row per
|
|
609
|
+
interval. 11.5 million points over four years become 3.2 million rows,
|
|
610
|
+
about 100 MB, at 10 s.
|
|
611
|
+
- **Overlap.** With `fillGapsOnly` (the default), a point is left out where the
|
|
612
|
+
vessel already has recorded positions in the same five minutes. Two
|
|
613
|
+
receivers' fixes interleaved in one series draw a sawtooth.
|
|
614
|
+
- **Doing it again.** Importing the same file again changes nothing, and a
|
|
615
|
+
stopped import can simply be restarted: the archive's dedup keys absorb the
|
|
616
|
+
rows already written.
|
|
617
|
+
- **Positions only.** The import takes a position and a time from each point;
|
|
618
|
+
depth, temperature or speed in a point's extensions are not imported. Depth
|
|
619
|
+
along the track beyond retention comes from live recording: list
|
|
620
|
+
`environment.depth.*` under **Archived own-vessel paths**, and retention keeps
|
|
621
|
+
it from then on.
|
|
622
|
+
- **Strict reading.** Coordinates must be decimals within range and times
|
|
623
|
+
`xsd:dateTime`; anything else is counted as invalid. A longitude written just
|
|
624
|
+
past ±180, as TimeZero does at the antimeridian, is wrapped. A file that is
|
|
625
|
+
not well-formed XML stops the import at the error, keeping the rows before
|
|
626
|
+
it.
|
|
627
|
+
|
|
548
628
|
## Export and backup
|
|
549
629
|
|
|
550
630
|
**Data Export** in the panel, and
|
|
@@ -556,7 +636,10 @@ CSV.
|
|
|
556
636
|
`GET /plugins/signalk-questdb/api/full-export/:table?from=…&to=…` streams one
|
|
557
637
|
whole table as Parquet, for snapshot and backup tooling that wants the full
|
|
558
638
|
content sliced into dedup-friendly shards (kopia, for instance). Tables:
|
|
559
|
-
`signalk`, `signalk_str`, `signalk_position`
|
|
639
|
+
`signalk`, `signalk_str`, `signalk_position`, `signalk_archive`, `signalk_position_archive`
|
|
640
|
+
(`/full-export/tables` lists them). The archive tables hold the own vessel's
|
|
641
|
+
history beyond retention and every imported track, so a backup that is meant to
|
|
642
|
+
outlive retention needs them too.
|
|
560
643
|
|
|
561
644
|
- `from` and `to` are optional but must be set together: omit both for the whole
|
|
562
645
|
table, or pass both as ISO 8601 timestamps for a half-open `[from, to)` window
|
|
@@ -598,6 +681,11 @@ default.
|
|
|
598
681
|
| GET | `/export?from=...&to=...&format=parquet` | Parquet or CSV export of the `signalk` numeric table (date range required) |
|
|
599
682
|
| GET | `/full-export/tables` | List the tables the full-export route serves |
|
|
600
683
|
| GET | `/full-export/:table?from=...&to=...` | Stream a table as Parquet; optional half-open `[from, to)` window for slicing into shards |
|
|
684
|
+
| GET | `/gpx-import` | The current or last GPX import |
|
|
685
|
+
| POST | `/gpx-import` | Announce an import: `{filename, size, tracks, fillGapsOnly}`, see Importing a GPX track |
|
|
686
|
+
| PUT | `/gpx-import/:id?offset=N` | One slice of the file, as `application/octet-stream`, at most 8 MiB |
|
|
687
|
+
| POST | `/gpx-import/:id/finish` | End the import once every byte is in; answers when QuestDB has applied it |
|
|
688
|
+
| DELETE | `/gpx-import/:id` | Abort the import; what it wrote stays |
|
|
601
689
|
|
|
602
690
|
## Deployment
|
|
603
691
|
|
|
@@ -687,7 +775,8 @@ and no client-visible error is raised anywhere. Left alone, recording stalls
|
|
|
687
775
|
silently for as long as nobody looks.
|
|
688
776
|
|
|
689
777
|
The plugin watches for this. A suspended table of its own (in external mode,
|
|
690
|
-
only the three tables above — never someone else's)
|
|
778
|
+
only the three tables above and the two archive tables — never someone else's)
|
|
779
|
+
is logged loudly, raised as
|
|
691
780
|
the Signal K notification `notifications.signalk-questdb.walSuspended`, and
|
|
692
781
|
shown on the status card as **WAL suspended**. Two causes need two remedies:
|
|
693
782
|
|
package/dist/api-contract.d.ts
CHANGED
|
@@ -268,3 +268,49 @@ export interface MigrationStatusResponse extends ApiError {
|
|
|
268
268
|
/** Present, when no run is active, if a stopped import can be continued. */
|
|
269
269
|
interrupted?: MigrationInterrupted;
|
|
270
270
|
}
|
|
271
|
+
/** What a GPX import has read and written so far. */
|
|
272
|
+
export interface GpxImportSummary {
|
|
273
|
+
/** Bytes of the document parsed. */
|
|
274
|
+
bytes: number;
|
|
275
|
+
tracks: number;
|
|
276
|
+
/** Points per track name, of the tracks selected for import. */
|
|
277
|
+
importedTracks: Record<string, number>;
|
|
278
|
+
/** Points per track name, of the tracks left out. */
|
|
279
|
+
skippedTracks: Record<string, number>;
|
|
280
|
+
/** Track points read, in every track. */
|
|
281
|
+
points: number;
|
|
282
|
+
/** Rows written to the archive. */
|
|
283
|
+
imported: number;
|
|
284
|
+
/** Points that fell in an interval already written from the same segment. */
|
|
285
|
+
thinned: number;
|
|
286
|
+
/** Points where the vessel already had recorded positions. */
|
|
287
|
+
alreadyRecorded: number;
|
|
288
|
+
/** Points with a position or time GPX cannot express, or none at all. */
|
|
289
|
+
invalid: number;
|
|
290
|
+
/** First and last imported instant, ISO. */
|
|
291
|
+
from: string | null;
|
|
292
|
+
to: string | null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* receiving: slices are accepted. finishing: the last slice is in and the
|
|
296
|
+
* import waits for QuestDB to apply it. The other three are final.
|
|
297
|
+
*/
|
|
298
|
+
export type GpxImportState = "receiving" | "finishing" | "finished" | "failed" | "aborted";
|
|
299
|
+
/** One GPX import, as /api/gpx-import reports it. */
|
|
300
|
+
export interface GpxImportStatus {
|
|
301
|
+
id: string;
|
|
302
|
+
filename: string;
|
|
303
|
+
/** Size of the file the client announced, in bytes. */
|
|
304
|
+
size: number;
|
|
305
|
+
/** Bytes accepted so far: where the next slice must start. */
|
|
306
|
+
received: number;
|
|
307
|
+
state: GpxImportState;
|
|
308
|
+
summary: GpxImportSummary;
|
|
309
|
+
error?: string;
|
|
310
|
+
startedAt: string;
|
|
311
|
+
updatedAt: string;
|
|
312
|
+
}
|
|
313
|
+
/** Every /api/gpx-import endpoint answers with the import it acted on. */
|
|
314
|
+
export interface GpxImportResponse extends ApiError {
|
|
315
|
+
import?: GpxImportStatus | null;
|
|
316
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { QueryClient } from "./query-client.js";
|
|
2
|
+
export declare const ARCHIVE_TABLES: {
|
|
3
|
+
readonly signalk: "signalk_archive";
|
|
4
|
+
readonly signalk_position: "signalk_position_archive";
|
|
5
|
+
};
|
|
6
|
+
export type ArchivedTable = keyof typeof ARCHIVE_TABLES;
|
|
7
|
+
/** Whether the archive holds rows of a table that a read must include. */
|
|
8
|
+
export interface ArchiveReader {
|
|
9
|
+
covers(table: ArchivedTable, from: string, to: string): boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const NO_ARCHIVE: ArchiveReader;
|
|
12
|
+
/**
|
|
13
|
+
* What a read selects from: the base table alone, or the base table and its
|
|
14
|
+
* archive merged in time order. The base table alone keeps the common case —
|
|
15
|
+
* a recent window, which the archive never reaches — on exactly the query it
|
|
16
|
+
* has always been.
|
|
17
|
+
*
|
|
18
|
+
* The merge has to declare `ts` its designated timestamp again, or SAMPLE BY
|
|
19
|
+
* and LATEST ON reject it, and that declaration is only true because of the
|
|
20
|
+
* ORDER BY. A time filter on the outer query still reaches both tables as an
|
|
21
|
+
* interval scan (verified with EXPLAIN on QuestDB 9.4 and 10.0).
|
|
22
|
+
*/
|
|
23
|
+
export declare function readFrom(table: ArchivedTable, withArchive: boolean): string;
|
|
24
|
+
/**
|
|
25
|
+
* The time span each archive table holds, cached so that deciding whether a
|
|
26
|
+
* read needs it costs no query. Refreshed after anything writes to the
|
|
27
|
+
* archive; until the first refresh nothing is archived as far as reads know.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ArchiveSpans implements ArchiveReader {
|
|
30
|
+
private readonly queryClient;
|
|
31
|
+
private readonly spans;
|
|
32
|
+
private read;
|
|
33
|
+
constructor(queryClient: Pick<QueryClient, "exec">);
|
|
34
|
+
/** Whether a refresh has read every archive table at least once. */
|
|
35
|
+
get known(): boolean;
|
|
36
|
+
refresh(): Promise<void>;
|
|
37
|
+
covers(table: ArchivedTable, from: string, to: string): boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Wait until QuestDB has applied everything committed to `table`'s WAL when
|
|
41
|
+
* this was called. A row is readable only once applied, and a suspended
|
|
42
|
+
* table never gets there, so that fails at once rather than at the deadline.
|
|
43
|
+
*/
|
|
44
|
+
export declare function waitForApply(queryClient: Pick<QueryClient, "exec">, table: string, timeoutMs: number, pollMs?: number): Promise<void>;
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The archive: what retention keeps of the own vessel once it drops the rest.
|
|
2
|
+
//
|
|
3
|
+
// Retention drops whole daily partitions, because QuestDB has no row-level
|
|
4
|
+
// DELETE, so "drop everything but the own track" cannot be a filter on the
|
|
5
|
+
// drop. Before a partition goes, the rows worth keeping are copied, thinned,
|
|
6
|
+
// into a table of their own that retention never touches. Reads add that
|
|
7
|
+
// table back in wherever it covers the asked range.
|
|
8
|
+
//
|
|
9
|
+
// The archive tables mirror their base tables' columns and dedup keys, so a
|
|
10
|
+
// copy that is repeated — a run interrupted between the copy and the drop —
|
|
11
|
+
// upserts onto itself instead of doubling rows. They are partitioned by month:
|
|
12
|
+
// an archive is sparse and lives for years, and a partition per day would
|
|
13
|
+
// leave thousands of near-empty ones behind.
|
|
14
|
+
import { isMissingTable } from "./schema-errors.js";
|
|
15
|
+
export const ARCHIVE_TABLES = {
|
|
16
|
+
signalk: "signalk_archive",
|
|
17
|
+
signalk_position: "signalk_position_archive",
|
|
18
|
+
};
|
|
19
|
+
const COLUMNS = {
|
|
20
|
+
signalk: "ts, path, context, source, value",
|
|
21
|
+
signalk_position: "ts, context, source, lat, lon",
|
|
22
|
+
};
|
|
23
|
+
export const NO_ARCHIVE = { covers: () => false };
|
|
24
|
+
/**
|
|
25
|
+
* What a read selects from: the base table alone, or the base table and its
|
|
26
|
+
* archive merged in time order. The base table alone keeps the common case —
|
|
27
|
+
* a recent window, which the archive never reaches — on exactly the query it
|
|
28
|
+
* has always been.
|
|
29
|
+
*
|
|
30
|
+
* The merge has to declare `ts` its designated timestamp again, or SAMPLE BY
|
|
31
|
+
* and LATEST ON reject it, and that declaration is only true because of the
|
|
32
|
+
* ORDER BY. A time filter on the outer query still reaches both tables as an
|
|
33
|
+
* interval scan (verified with EXPLAIN on QuestDB 9.4 and 10.0).
|
|
34
|
+
*/
|
|
35
|
+
export function readFrom(table, withArchive) {
|
|
36
|
+
if (!withArchive)
|
|
37
|
+
return table;
|
|
38
|
+
const columns = COLUMNS[table];
|
|
39
|
+
return (`((SELECT ${columns} FROM ${table} UNION ALL ` +
|
|
40
|
+
`SELECT ${columns} FROM ${ARCHIVE_TABLES[table]}) ORDER BY ts) TIMESTAMP(ts)`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The time span each archive table holds, cached so that deciding whether a
|
|
44
|
+
* read needs it costs no query. Refreshed after anything writes to the
|
|
45
|
+
* archive; until the first refresh nothing is archived as far as reads know.
|
|
46
|
+
*/
|
|
47
|
+
export class ArchiveSpans {
|
|
48
|
+
queryClient;
|
|
49
|
+
spans = new Map();
|
|
50
|
+
read = false;
|
|
51
|
+
constructor(queryClient) {
|
|
52
|
+
this.queryClient = queryClient;
|
|
53
|
+
}
|
|
54
|
+
/** Whether a refresh has read every archive table at least once. */
|
|
55
|
+
get known() {
|
|
56
|
+
return this.read;
|
|
57
|
+
}
|
|
58
|
+
async refresh() {
|
|
59
|
+
for (const table of Object.keys(ARCHIVE_TABLES)) {
|
|
60
|
+
try {
|
|
61
|
+
const result = await this.queryClient.exec(`SELECT min(ts), max(ts) FROM ${ARCHIVE_TABLES[table]}`);
|
|
62
|
+
const [from, to] = result.dataset[0] ?? [];
|
|
63
|
+
if (typeof from === "string" && typeof to === "string") {
|
|
64
|
+
this.spans.set(table, [Date.parse(from), Date.parse(to)]);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.spans.delete(table);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// A missing table archives nothing. Any other failure keeps the span
|
|
72
|
+
// already known: forgetting it would hide archived history from
|
|
73
|
+
// every read until the next refresh.
|
|
74
|
+
if (isMissingTable(err))
|
|
75
|
+
this.spans.delete(table);
|
|
76
|
+
else
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.read = true;
|
|
81
|
+
}
|
|
82
|
+
covers(table, from, to) {
|
|
83
|
+
const span = this.spans.get(table);
|
|
84
|
+
if (!span)
|
|
85
|
+
return false;
|
|
86
|
+
return Date.parse(from) <= span[1] && Date.parse(to) >= span[0];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Wait until QuestDB has applied everything committed to `table`'s WAL when
|
|
91
|
+
* this was called. A row is readable only once applied, and a suspended
|
|
92
|
+
* table never gets there, so that fails at once rather than at the deadline.
|
|
93
|
+
*/
|
|
94
|
+
export async function waitForApply(queryClient, table, timeoutMs, pollMs = 1000) {
|
|
95
|
+
const state = async () => {
|
|
96
|
+
const result = await queryClient.exec(`SELECT suspended, writerTxn, sequencerTxn FROM wal_tables() WHERE name = '${table}'`);
|
|
97
|
+
const [suspended, writerTxn, sequencerTxn] = result.dataset[0] ?? [];
|
|
98
|
+
if (typeof writerTxn !== "number" || typeof sequencerTxn !== "number") {
|
|
99
|
+
throw new Error(`${table} is not a WAL table`);
|
|
100
|
+
}
|
|
101
|
+
if (suspended === true) {
|
|
102
|
+
throw new Error(`${table} is suspended, so the rows cannot be applied`);
|
|
103
|
+
}
|
|
104
|
+
return { writerTxn, sequencerTxn };
|
|
105
|
+
};
|
|
106
|
+
const target = (await state()).sequencerTxn;
|
|
107
|
+
const deadline = Date.now() + timeoutMs;
|
|
108
|
+
for (;;) {
|
|
109
|
+
if ((await state()).writerTxn >= target)
|
|
110
|
+
return;
|
|
111
|
+
if (Date.now() > deadline) {
|
|
112
|
+
throw new Error(`${table} did not apply the rows within the timeout`);
|
|
113
|
+
}
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,oDAAoD;AACpD,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,+EAA+E;AAC/E,0EAA0E;AAC1E,6CAA6C;AAG7C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,OAAO,EAAE,iBAAiB;IAC1B,gBAAgB,EAAE,0BAA0B;CACpC,CAAC;AAIX,MAAM,OAAO,GAAkC;IAC7C,OAAO,EAAE,kCAAkC;IAC3C,gBAAgB,EAAE,+BAA+B;CAClD,CAAC;AAOF,MAAM,CAAC,MAAM,UAAU,GAAkB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAoB,EAAE,WAAoB;IACjE,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,CACL,YAAY,OAAO,SAAS,KAAK,aAAa;QAC9C,UAAU,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,8BAA8B,CAC9E,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,YAAY;IAIM;IAHZ,KAAK,GAAG,IAAI,GAAG,EAAmC,CAAC;IAC5D,IAAI,GAAG,KAAK,CAAC;IAErB,YAA6B,WAAsC;QAAtC,gBAAW,GAAX,WAAW,CAA2B;IAAG,CAAC;IAEvE,oEAAoE;IACpE,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAoB,EAAE,CAAC;YACnE,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CACxC,gCAAgC,cAAc,CAAC,KAAK,CAAC,EAAE,CACxD,CAAC;gBACF,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC3C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;oBACvD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC5D,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC3B,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,qEAAqE;gBACrE,gEAAgE;gBAChE,qCAAqC;gBACrC,IAAI,cAAc,CAAC,GAAG,CAAC;oBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;;oBAC7C,MAAM,GAAG,CAAC;YACjB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,MAAM,CAAC,KAAoB,EAAE,IAAY,EAAE,EAAU;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QACxB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,WAAsC,EACtC,KAAa,EACb,SAAiB,EACjB,MAAM,GAAG,IAAI;IAEb,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE;QACvB,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,CACnC,6EAA6E,KAAK,GAAG,CACtF,CAAC;QACF,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACrE,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,qBAAqB,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,8CAA8C,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IACrC,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC;IAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC;QACR,IAAI,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,SAAS,IAAI,MAAM;YAAE,OAAO;QAChD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,4CAA4C,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export declare const ConfigSchema: Type.TObject<{
|
|
|
23
23
|
historySourcePolicyAll: Type.TBoolean;
|
|
24
24
|
enableConsole: Type.TBoolean;
|
|
25
25
|
retentionDays: Type.TNumber;
|
|
26
|
+
archiveOwnTrack: Type.TBoolean;
|
|
27
|
+
archivePaths: Type.TArray<Type.TString>;
|
|
28
|
+
archiveResolutionSeconds: Type.TNumber;
|
|
26
29
|
networkName: Type.TString;
|
|
27
30
|
exposeToContainers: Type.TBoolean;
|
|
28
31
|
compression: Type.TUnion<[Type.TLiteral<"none">, Type.TLiteral<"lz4">, Type.TLiteral<"zstd">]>;
|
package/dist/config/schema.js
CHANGED
|
@@ -106,6 +106,22 @@ export const ConfigSchema = Type.Object({
|
|
|
106
106
|
default: 0,
|
|
107
107
|
title: "Retention (days, 0 = keep forever)",
|
|
108
108
|
}),
|
|
109
|
+
archiveOwnTrack: Type.Boolean({
|
|
110
|
+
default: true,
|
|
111
|
+
title: "Keep own track beyond retention",
|
|
112
|
+
description: "Before retention drops a day, copy the own vessel's positions into an archive that retention never touches, thinned to the archive resolution. History reads include the archive, so the own track stays available however old it is.",
|
|
113
|
+
}),
|
|
114
|
+
archivePaths: Type.Array(Type.String(), {
|
|
115
|
+
default: [],
|
|
116
|
+
title: "Archived own-vessel paths",
|
|
117
|
+
description: "Numeric paths of the own vessel to archive the same way, as glob patterns, e.g. environment.depth.*. Other vessels are never archived.",
|
|
118
|
+
}),
|
|
119
|
+
archiveResolutionSeconds: Type.Number({
|
|
120
|
+
default: 10,
|
|
121
|
+
minimum: 0,
|
|
122
|
+
title: "Archive resolution (seconds)",
|
|
123
|
+
description: "Archived rows are thinned to the first sample in each interval of this length. 0 archives every recorded row.",
|
|
124
|
+
}),
|
|
109
125
|
networkName: Type.String({
|
|
110
126
|
default: "sk-network",
|
|
111
127
|
title: "Container network",
|
|
@@ -168,6 +184,17 @@ export function normalizeConfig(config) {
|
|
|
168
184
|
// than restoreOnStart: the console only ever does what an authenticated
|
|
169
185
|
// admin types, so a missing key should not hide the tool.
|
|
170
186
|
enableConsole: config.enableConsole !== false,
|
|
187
|
+
// On unless explicitly disabled: turning retention on must not silently
|
|
188
|
+
// drop the own track along with everything else.
|
|
189
|
+
archiveOwnTrack: config.archiveOwnTrack !== false,
|
|
190
|
+
archivePaths: Array.isArray(config.archivePaths)
|
|
191
|
+
? config.archivePaths.filter((p) => typeof p === "string" && p.trim() !== "")
|
|
192
|
+
: [],
|
|
193
|
+
archiveResolutionSeconds: typeof config.archiveResolutionSeconds === "number" &&
|
|
194
|
+
Number.isFinite(config.archiveResolutionSeconds) &&
|
|
195
|
+
config.archiveResolutionSeconds >= 0
|
|
196
|
+
? Math.floor(config.archiveResolutionSeconds)
|
|
197
|
+
: 10,
|
|
171
198
|
// Missing resource limits get the schema defaults. Config-panel saves
|
|
172
199
|
// used to drop these keys entirely (issue #98), which ran QuestDB with
|
|
173
200
|
// NO memory cap instead of the promised 768m — this backfill repairs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAU,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACtC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,WAAW;QACpB,KAAK,EAAE,cAAc;QACrB,WAAW,EACT,oHAAoH;KACvH,CAAC;IACF,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC;QAC1B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,4HAA4H;KAC/H,CAAC;IACF,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC;QAC3B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,4HAA4H;KAC/H,CAAC;IACF,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC;QACzB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,uHAAuH;KAC1H,CAAC;IACF,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC;QAC1B,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,uBAAuB;KAC/B,CAAC;IAEF,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,gDAAgD;QACvD,WAAW,EAAE,oDAAoD;KAClE,CAAC;IAEF,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC9B,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,2JAA2J;KAC9J,CAAC;IAEF,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC;QAC3B,OAAO,EAAE,GAAG;QACZ,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,sEAAsE;KACzE,CAAC;IAEF,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC;QACtB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE;YACnE,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,aAAa;SACrB,CAAC;QACF,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;YAC/B,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,gCAAgC;YACvC,WAAW,EAAE,8CAA8C;SAC5D,CAAC;KACH,CAAC;IAEF,mBAAmB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC/B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,gMAAgM;KACnM,CAAC;IAEF,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC9B,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,GAAG;QACZ,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,wbAAwb;KAC3b,CAAC;IAEF,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;QACvD,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,8BAA8B;QACrC,WAAW,EACT,gGAAgG;KACnG,CAAC;IAEF,yBAAyB,EAAE,IAAI,CAAC,MAAM,CAAC;QACrC,OAAO,EAAE,GAAG;QACZ,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,+BAA+B;QACtC,WAAW,EACT,8YAA8Y;KACjZ,CAAC;IAEF,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC;QACvB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,mBAAmB;KAC3B,CAAC;IACF,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC;QACzB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,oBAAoB;KAC5B,CAAC;IAEF,cAAc,EAAE,IAAI,CAAC,OAAO,CAAC;QAC3B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,0RAA0R;KAC7R,CAAC;IACF,oBAAoB,EAAE,IAAI,CAAC,MAAM,CAAC;QAChC,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,+OAA+O;KAClP,CAAC;IAEF,sBAAsB,EAAE,IAAI,CAAC,OAAO,CAAC;QACnC,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,2CAA2C;QAClD,WAAW,EACT,8cAA8c;KACjd,CAAC;IAEF,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC;QAC1B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EACT,uOAAuO;KAC1O,CAAC;IAEF,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC;QACzB,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,oCAAoC;KAC5C,CAAC;IAEF,eAAe,EAAE,IAAI,CAAC,OAAO,CAAC;QAC5B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,iCAAiC;QACxC,WAAW,EACT,uOAAuO;KAC1O,CAAC;IAEF,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;QACtC,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,wIAAwI;KAC3I,CAAC;IAEF,wBAAwB,EAAE,IAAI,CAAC,MAAM,CAAC;QACpC,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,8BAA8B;QACrC,WAAW,EACT,+GAA+G;KAClH,CAAC;IAEF,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,YAAY;QACrB,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,4NAA4N;KAC/N,CAAC;IAEF,kBAAkB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC/B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,8OAA8O;KACjP,CAAC;IAEF,WAAW,EAAE,IAAI,CAAC,KAAK,CACrB,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EACjE;QACE,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,wFAAwF;KAC3F,CACF;IACD,gBAAgB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC5B,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,sCAAsC;KACpD,CAAC;CACH,CAAC,CAAC;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE;YACV,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,IAAI,SAAS;YAC1C,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;SACtC;QACD,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;QACzC,uEAAuE;QACvE,wEAAwE;QACxE,yBAAyB,EACvB,OAAO,MAAM,CAAC,yBAAyB,KAAK,QAAQ;YACpD,MAAM,CAAC,yBAAyB,IAAI,CAAC;YACnC,CAAC,CAAC,MAAM,CAAC,yBAAyB;YAClC,CAAC,CAAC,GAAG;QACT,mEAAmE;QACnE,uEAAuE;QACvE,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,KAAK;QACvC,YAAY,EAAE,MAAM,CAAC,YAAY,KAAK,KAAK;QAC3C,qEAAqE;QACrE,kEAAkE;QAClE,sEAAsE;QACtE,mDAAmD;QACnD,cAAc,EAAE,MAAM,CAAC,cAAc,KAAK,IAAI;QAC9C,oBAAoB,EAClB,MAAM,CAAC,oBAAoB,IAAI,MAAM,CAAC,oBAAoB,GAAG,CAAC;YAC5D,CAAC,CAAC,MAAM,CAAC,oBAAoB;YAC7B,CAAC,CAAC,CAAC;QACP,uEAAuE;QACvE,wEAAwE;QACxE,0DAA0D;QAC1D,aAAa,EAAE,MAAM,CAAC,aAAa,KAAK,KAAK;QAC7C,wEAAwE;QACxE,iDAAiD;QACjD,eAAe,EAAE,MAAM,CAAC,eAAe,KAAK,KAAK;QACjD,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;YAC9C,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CACxB,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAC7D;YACH,CAAC,CAAC,EAAE;QACN,wBAAwB,EACtB,OAAO,MAAM,CAAC,wBAAwB,KAAK,QAAQ;YACnD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,wBAAwB,CAAC;YAChD,MAAM,CAAC,wBAAwB,IAAI,CAAC;YAClC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,wBAAwB,CAAC;YAC7C,CAAC,CAAC,EAAE;QACR,sEAAsE;QACtE,uEAAuE;QACvE,qEAAqE;QACrE,kEAAkE;QAClE,qEAAqE;QACrE,qBAAqB;QACrB,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,IAAI,MAAM;QACvD,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,GAAG;KAC/C,CAAC;AACJ,CAAC"}
|
|
@@ -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>;
|