signalk-questdb 2.1.4 → 2.1.5-beta.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/README.md +112 -27
- package/dist/api-contract.d.ts +45 -0
- package/dist/configpanel/responses.d.ts +28 -1
- package/dist/configpanel/responses.js +74 -0
- package/dist/configpanel/responses.js.map +1 -1
- package/dist/ilp-writer.d.ts +52 -0
- package/dist/ilp-writer.js +88 -7
- package/dist/ilp-writer.js.map +1 -1
- package/dist/index.js +477 -45
- package/dist/index.js.map +1 -1
- package/dist/migration-checkpoint.d.ts +170 -0
- package/dist/migration-checkpoint.js +276 -0
- package/dist/migration-checkpoint.js.map +1 -0
- package/dist/migration-cleanup.d.ts +48 -0
- package/dist/migration-cleanup.js +155 -0
- package/dist/migration-cleanup.js.map +1 -0
- package/dist/migration.d.ts +134 -17
- package/dist/migration.js +694 -158
- package/dist/migration.js.map +1 -1
- package/dist/questdb-mount.d.ts +106 -0
- package/dist/questdb-mount.js +173 -0
- package/dist/questdb-mount.js.map +1 -0
- package/package.json +2 -2
- package/public/assets/AppPanel-p27aQlBS.js +1 -0
- package/public/assets/PluginConfigurationPanel-BO_4OxBo.js +9 -0
- package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-C2P0b0We.js +1 -0
- package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__275086164343916__loadShare__react__loadShare__.js-BWO9MGdR.js +1 -0
- package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__275086164343916__prebuild__react__prebuild__-D2g10OtU.js +1 -0
- package/public/assets/{dist-BgxWXJ5y.js → dist-DT1ZTQL0.js} +1 -1
- package/public/assets/{hostInit-CyS9_M1c.js → hostInit-CkflWBLR.js} +1 -1
- package/public/assets/index-CiKXXQrp.js +2 -0
- package/public/assets/pendingShares-BI27NR0_.js +1 -0
- package/public/assets/{vite-preload-helper-HclGiUj8.js → vite-preload-helper-uBIymjUX.js} +1 -1
- package/public/remoteEntry.js +2 -2
- package/public/assets/AppPanel-CNFtJpeq.js +0 -1
- package/public/assets/PluginConfigurationPanel-CR4mD9IH.js +0 -9
- package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-7tb3Q-Bx.js +0 -1
- package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__1__loadShare__react__loadShare__.js-DzhF4wXp.js +0 -1
- package/public/assets/index-Dh-t5VMR.js +0 -1
- package/public/assets/virtualExposes-BBIkbTtD.js +0 -1
package/README.md
CHANGED
|
@@ -158,24 +158,28 @@ Registered via `app.registerHistoryProvider()`. Supports playback at configurabl
|
|
|
158
158
|
|
|
159
159
|
All mounted at `/plugins/signalk-questdb/api/`:
|
|
160
160
|
|
|
161
|
-
| Method | Path | Description
|
|
162
|
-
| ------ | ---------------------------------------- |
|
|
163
|
-
| GET | `/status` | QuestDB health, row counts, active paths
|
|
164
|
-
| GET | `/query?sql=...` | Read-only SQL proxy (DDL/DML blocked)
|
|
165
|
-
| GET | `/paths` | All recorded paths with row counts and time range
|
|
166
|
-
| GET | `/versions` | QuestDB releases from GitHub (for version picker)
|
|
167
|
-
| GET | `/update/check` | Compare running version against latest release
|
|
168
|
-
| POST | `/update/apply` | Pull latest image, recreate container, reconnect
|
|
169
|
-
| POST | `/purge-data` | Remove the QuestDB container and delete all its data (rootless-Podman-safe)
|
|
170
|
-
| GET | `/migration/detect` | Detect InfluxDB (supports `?url=` for remote)
|
|
171
|
-
| POST | `/migration/buckets` | List buckets (2.x) or databases (1.x); credentials in the body, never the query string
|
|
172
|
-
| POST | `/migration/measurements` | List measurements and their field keys in a bucket/database
|
|
173
|
-
| POST | `/migration/
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
| GET | `/
|
|
161
|
+
| Method | Path | Description |
|
|
162
|
+
| ------ | ---------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
163
|
+
| GET | `/status` | QuestDB health, row counts, active paths |
|
|
164
|
+
| GET | `/query?sql=...` | Read-only SQL proxy (DDL/DML blocked) |
|
|
165
|
+
| GET | `/paths` | All recorded paths with row counts and time range |
|
|
166
|
+
| GET | `/versions` | QuestDB releases from GitHub (for version picker) |
|
|
167
|
+
| GET | `/update/check` | Compare running version against latest release |
|
|
168
|
+
| POST | `/update/apply` | Pull latest image, recreate container, reconnect |
|
|
169
|
+
| POST | `/purge-data` | Remove the QuestDB container and delete all its data (rootless-Podman-safe) |
|
|
170
|
+
| GET | `/migration/detect` | Detect InfluxDB (supports `?url=` for remote) |
|
|
171
|
+
| POST | `/migration/buckets` | List buckets (2.x) or databases (1.x); credentials in the body, never the query string |
|
|
172
|
+
| POST | `/migration/measurements` | List measurements and their field keys in a bucket/database |
|
|
173
|
+
| POST | `/migration/contexts` | List the vessels (`context` tag values) a bucket/database holds, and which is this server |
|
|
174
|
+
| POST | `/migration/start` | Start an import, or continue a stopped one with `{"resume": true}`; returns immediately |
|
|
175
|
+
| GET | `/migration/status` | Progress and state of the current/last import, and any stopped import that can resume |
|
|
176
|
+
| POST | `/migration/cancel` | Cancel the running import; any saved position is kept |
|
|
177
|
+
| POST | `/migration/discard` | Forget a stopped import's saved position, so the next start begins again |
|
|
178
|
+
| GET | `/migration/legacy-rows` | Count rows an import made before 2.1.5 filed under suffixed paths |
|
|
179
|
+
| POST | `/migration/legacy-rows/remove` | Rebuild the string table without those rows; recording continues meanwhile |
|
|
180
|
+
| GET | `/export?from=...&to=...&format=parquet` | Parquet or CSV export of the `signalk` numeric table (date range required) |
|
|
181
|
+
| GET | `/full-export/tables` | List tables exposed by the per-table full-export route |
|
|
182
|
+
| GET | `/full-export/:table?from=...&to=...` | Stream a table as Parquet. Optional half-open `[from, to)` range for slicing into shards |
|
|
179
183
|
|
|
180
184
|
### `/full-export/:table` (since 0.4.0)
|
|
181
185
|
|
|
@@ -206,20 +210,79 @@ existing InfluxDB into QuestDB. It supports both InfluxDB 1.x (InfluxQL) and
|
|
|
206
210
|
How the data maps:
|
|
207
211
|
|
|
208
212
|
- A measurement with the conventional `value` field becomes the Signal K path
|
|
209
|
-
of the same name
|
|
210
|
-
`
|
|
213
|
+
of the same name. So do the typed fields signalk-to-influxdb 1.x writes
|
|
214
|
+
(`stringValue`, `boolValue`, `jsonValue`). A measurement with any other named
|
|
215
|
+
fields becomes `measurement.field` paths, so two fields cannot overwrite each
|
|
216
|
+
other.
|
|
211
217
|
- Numbers go to `signalk`, strings and booleans to `signalk_str` (booleans
|
|
212
|
-
tagged `value_kind=boolean`)
|
|
213
|
-
|
|
218
|
+
tagged `value_kind=boolean`). A `jsonValue` is decoded first: an object is
|
|
219
|
+
recorded as its scalar leaves, the same as live data.
|
|
220
|
+
- Positions go to `signalk_position`, whether stored as a `jsonValue`
|
|
221
|
+
(signalk-to-influxdb 1.x) or as a `lat`/`lon` or `latitude`/`longitude` field
|
|
222
|
+
pair (signalk-to-influxdb2).
|
|
223
|
+
- Both writers tag every point with the **context** of the vessel it belongs
|
|
224
|
+
to, so a source that recorded other vessels holds several. The panel lists
|
|
225
|
+
them and asks which is yours — preselected when the source knows this
|
|
226
|
+
server's identity, or holds only one vessel — and that vessel's history is
|
|
227
|
+
imported as `self`. The others are imported under their own contexts, as the
|
|
228
|
+
history API serves them, or left out if you untick that. The recording
|
|
229
|
+
server's identity is usually not this server's (a new install has a new
|
|
230
|
+
one), which is why it is chosen rather than matched.
|
|
214
231
|
- Rows keep their **original nanosecond timestamps**, so imported history sorts
|
|
215
232
|
and aggregates alongside live data.
|
|
233
|
+
- History is **streamed**: read in batches and written before more is asked
|
|
234
|
+
for, pausing while QuestDB catches up. Memory use stays flat however dense
|
|
235
|
+
the source is, so a large import is safe on a small board.
|
|
216
236
|
- Every imported row is tagged `source=influxdb-import`, which makes it
|
|
217
237
|
distinguishable from live recording — and because the tables deduplicate on
|
|
218
238
|
`(ts, path, context, source)`, **re-running the same range overwrites rather
|
|
219
|
-
than duplicating**.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
239
|
+
than duplicating**.
|
|
240
|
+
|
|
241
|
+
### Imports made before 2.1.5
|
|
242
|
+
|
|
243
|
+
Imports from a signalk-to-influxdb 1.x database made before 2.1.5 filed every
|
|
244
|
+
string, boolean and position under a path suffixed with the InfluxDB field
|
|
245
|
+
name — `navigation.state.stringValue`, `steering.autopilot.engaged.boolValue`,
|
|
246
|
+
`navigation.position.jsonValue` — which nothing reads. Running the import again
|
|
247
|
+
writes them under their real paths but cannot remove the old rows: the path is
|
|
248
|
+
part of the deduplication key, and QuestDB has no row delete. When such rows
|
|
249
|
+
exist, the panel says how many and offers **Remove these rows**, which rebuilds
|
|
250
|
+
`signalk_str` without them. Recording continues while it runs; only the swap
|
|
251
|
+
of the rebuilt table for the old one holds the writer, for a few seconds, and
|
|
252
|
+
rows recorded during the rebuild are carried across. Should recording be fast
|
|
253
|
+
enough to fill the writer's buffer during those seconds, the samples it could
|
|
254
|
+
not keep are counted and the panel says how many. Run the import again
|
|
255
|
+
afterwards.
|
|
256
|
+
|
|
257
|
+
### Resuming an interrupted import
|
|
258
|
+
|
|
259
|
+
A large import runs for hours. If it stops part-way — cancelled, failed, or the
|
|
260
|
+
Signal K server restarted — the panel shows what was interrupted and offers
|
|
261
|
+
**Resume import**, which continues from the saved position instead of starting
|
|
262
|
+
again at the first measurement. It needs only the credentials, if InfluxDB asks
|
|
263
|
+
for any: the source and range are remembered, credentials never are. Starting
|
|
264
|
+
the identical import again the ordinary way continues it too. **Discard saved
|
|
265
|
+
position** starts over.
|
|
266
|
+
|
|
267
|
+
A position is saved only once QuestDB has been asked for the newest row written
|
|
268
|
+
before it and has it — ILP gives no acknowledgment, so the plugin reads the row
|
|
269
|
+
back — and once the position is about a minute old. The first keeps a stalled
|
|
270
|
+
or restarted QuestDB from letting the position past rows it never took, however
|
|
271
|
+
long the stall. The minute covers what a committed row still has to survive: a
|
|
272
|
+
crash of either process, and on a power cut the kernel's default write-back
|
|
273
|
+
timing for a QuestDB running on its default `nosync` — an assumption, not a
|
|
274
|
+
guarantee, which is why an external QuestDB should run with `sync` like the
|
|
275
|
+
managed container does (see
|
|
276
|
+
[Durability on power loss](#durability-on-power-loss)). The position file is
|
|
277
|
+
synced and renamed into place, so a power cut leaves the previous position or
|
|
278
|
+
the new one, never a damaged one; should none survive, the import starts again,
|
|
279
|
+
which is always safe. The cost is that a resumed import repeats up to a minute
|
|
280
|
+
or so of work, which the deduplication above makes harmless. An import shorter
|
|
281
|
+
than that saves nothing, and is simply run again.
|
|
282
|
+
|
|
283
|
+
Anything that cannot be mapped (a gap, an unsupported value type, a `jsonValue`
|
|
284
|
+
that is not valid JSON, a latitude with no matching longitude) is counted in the
|
|
285
|
+
run's `skipped` total rather than
|
|
223
286
|
being dropped silently.
|
|
224
287
|
|
|
225
288
|
## Configuration
|
|
@@ -681,7 +744,29 @@ you had chosen, and on servers with security enabled it never worked at all.
|
|
|
681
744
|
|
|
682
745
|
## Data Storage
|
|
683
746
|
|
|
684
|
-
QuestDB data is
|
|
747
|
+
QuestDB's data root is the plugin's own data directory,
|
|
748
|
+
`<config>/plugin-config-data/signalk-questdb`, whatever the deployment. Data
|
|
749
|
+
survives container restarts, image upgrades, and plugin disable/enable cycles.
|
|
750
|
+
How it reaches the container differs:
|
|
751
|
+
|
|
752
|
+
- **Bare-metal Signal K**: that directory, mounted at `/var/lib/questdb`.
|
|
753
|
+
- **Signal K in a container with its config directory bind-mounted**: the
|
|
754
|
+
exact host path of that directory, mounted at `/var/lib/questdb`.
|
|
755
|
+
- **Signal K in a container with its config directory on a named volume**:
|
|
756
|
+
the volume cannot be mounted from a subdirectory, so it is mounted whole at
|
|
757
|
+
`/signalk-vols/<volume>` and QuestDB is pointed at the plugin's directory
|
|
758
|
+
inside it (`QUESTDB_DATA_DIR`). A database found at the volume's root — where
|
|
759
|
+
a whole-volume mount put it — is moved into the plugin's directory on start;
|
|
760
|
+
both are the same volume, so nothing is copied. A purge deletes the
|
|
761
|
+
directory from the Signal K process, since the runtime cannot mount the
|
|
762
|
+
volume by Signal K's path; where QuestDB's user owns it and the Signal K
|
|
763
|
+
user cannot delete it, the purge says so and the directory is deleted by
|
|
764
|
+
hand.
|
|
765
|
+
|
|
766
|
+
QuestDB's entrypoint makes its data root its own on start, so the plugin keeps
|
|
767
|
+
the one file it writes alongside — the import checkpoint,
|
|
768
|
+
`signalk-questdb.influx-import-checkpoint.json` — next to the directory rather
|
|
769
|
+
than in it. A purge removes it, whether or not the data could be deleted.
|
|
685
770
|
|
|
686
771
|
## Grafana Integration
|
|
687
772
|
|
package/dist/api-contract.d.ts
CHANGED
|
@@ -183,6 +183,24 @@ export interface MigrationMeasurement {
|
|
|
183
183
|
export interface MigrationMeasurementsResponse extends ApiError {
|
|
184
184
|
measurements: MigrationMeasurement[];
|
|
185
185
|
}
|
|
186
|
+
/** POST /api/migration/contexts: the vessels a bucket/database holds. */
|
|
187
|
+
export interface MigrationContextsResponse extends ApiError {
|
|
188
|
+
/** Distinct `context` tag values, sorted. Empty for a non-Signal K schema. */
|
|
189
|
+
contexts: string[];
|
|
190
|
+
/** The one that is this server's own vessel, when it is among them. */
|
|
191
|
+
self?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* GET /api/migration/legacy-rows: rows an import made before 2.1.5 filed under
|
|
195
|
+
* suffixed paths, and POST /api/migration/legacy-rows/remove: how many it
|
|
196
|
+
* removed.
|
|
197
|
+
*/
|
|
198
|
+
export interface LegacyImportRowsResponse extends ApiError {
|
|
199
|
+
rows: number;
|
|
200
|
+
/** After a removal: live samples the writer's cap discarded while it was
|
|
201
|
+
* held for the table swap. Normally 0. */
|
|
202
|
+
dropped?: number;
|
|
203
|
+
}
|
|
186
204
|
/** Per-run counters, also the shape the panel renders as progress. */
|
|
187
205
|
export interface MigrationProgress {
|
|
188
206
|
/** Rows read from InfluxDB. */
|
|
@@ -201,6 +219,29 @@ export interface MigrationProgress {
|
|
|
201
219
|
currentWindowStart?: string;
|
|
202
220
|
}
|
|
203
221
|
export type MigrationRunState = "running" | "done" | "failed" | "cancelled";
|
|
222
|
+
/** Where a resumed run picked up. */
|
|
223
|
+
export interface MigrationResumePoint {
|
|
224
|
+
/** Measurement that was under way; absent if it stopped between two. */
|
|
225
|
+
measurement?: string;
|
|
226
|
+
/** First window not yet imported for that measurement, ISO. */
|
|
227
|
+
windowStart?: string;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* An import that stopped before it finished and can be continued. Carries the
|
|
231
|
+
* import's own parameters — never its credentials — so it can be resumed after
|
|
232
|
+
* a server restart, when the panel no longer remembers what was entered.
|
|
233
|
+
*/
|
|
234
|
+
export interface MigrationInterrupted extends MigrationResumePoint {
|
|
235
|
+
url: string;
|
|
236
|
+
type: string;
|
|
237
|
+
bucket: string;
|
|
238
|
+
from: string;
|
|
239
|
+
to: string;
|
|
240
|
+
/** Measurements already imported in full. */
|
|
241
|
+
measurementsDone: number;
|
|
242
|
+
/** When the saved position was reached, ISO. */
|
|
243
|
+
updatedAt: string;
|
|
244
|
+
}
|
|
204
245
|
/** GET /api/migration/status, and the body of POST /api/migration/start. */
|
|
205
246
|
export interface MigrationStatusResponse extends ApiError {
|
|
206
247
|
/** Absent when no run has ever been started this process lifetime. */
|
|
@@ -215,5 +256,9 @@ export interface MigrationStatusResponse extends ApiError {
|
|
|
215
256
|
progress: MigrationProgress;
|
|
216
257
|
/** Set when state is "failed". */
|
|
217
258
|
error?: string;
|
|
259
|
+
/** Set when this run continued an earlier one rather than starting over. */
|
|
260
|
+
resumedFrom?: MigrationResumePoint;
|
|
218
261
|
};
|
|
262
|
+
/** Present, when no run is active, if a stopped import can be continued. */
|
|
263
|
+
interrupted?: MigrationInterrupted;
|
|
219
264
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MigrationBucket, MigrationMeasurement, MigrationSource, MigrationStatusResponse } from "../api-contract.js";
|
|
1
|
+
import type { MigrationBucket, MigrationContextsResponse, MigrationMeasurement, MigrationSource, MigrationStatusResponse } from "../api-contract.js";
|
|
2
2
|
/**
|
|
3
3
|
* GET /api/migration/detect returns `{ sources: [...] }`. A body without it
|
|
4
4
|
* means "nothing detected", which is also what an empty array means — so the
|
|
@@ -21,6 +21,33 @@ export declare function toMigrationMeasurements(body: unknown): MigrationMeasure
|
|
|
21
21
|
* render as undefined and printing "NaN%".
|
|
22
22
|
*/
|
|
23
23
|
export declare function toMigrationStatus(body: unknown): MigrationStatusResponse["run"] | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* GET /api/migration/legacy-rows and its POST remove: a row count, or 0, and
|
|
26
|
+
* after a removal the live samples dropped during the swap, normally 0.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toLegacyImportRows(body: unknown): {
|
|
29
|
+
rows: number;
|
|
30
|
+
dropped: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* POST /api/migration/contexts: the vessels the source holds. A body without
|
|
34
|
+
* a proper list reads as "none found", which leaves the choice out of the form
|
|
35
|
+
* rather than rendering a broken one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toMigrationContexts(body: unknown): Pick<MigrationContextsResponse, "contexts" | "self">;
|
|
38
|
+
/**
|
|
39
|
+
* An ISO instant from the server, in the reader's own locale and time zone.
|
|
40
|
+
* Anything that is not a date is shown as it came rather than as
|
|
41
|
+
* "Invalid Date".
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatDateTime(iso: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The stopped import a status response offers to continue, if any.
|
|
46
|
+
*
|
|
47
|
+
* Everything the notice prints is required: a partial one would render as
|
|
48
|
+
* "import of undefined", so a body missing any of it yields no notice at all.
|
|
49
|
+
*/
|
|
50
|
+
export declare function toMigrationInterrupted(body: unknown): MigrationStatusResponse["interrupted"] | undefined;
|
|
24
51
|
/**
|
|
25
52
|
* Validate the two `datetime-local` values an import range is entered as.
|
|
26
53
|
*
|
|
@@ -85,6 +85,12 @@ export function toMigrationStatus(body) {
|
|
|
85
85
|
startedAt: str(run.startedAt) ?? "",
|
|
86
86
|
finishedAt: str(run.finishedAt),
|
|
87
87
|
error: str(run.error),
|
|
88
|
+
resumedFrom: isRecord(run.resumedFrom)
|
|
89
|
+
? {
|
|
90
|
+
measurement: str(run.resumedFrom.measurement),
|
|
91
|
+
windowStart: str(run.resumedFrom.windowStart),
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
88
94
|
progress: {
|
|
89
95
|
read: num(p.read),
|
|
90
96
|
written: num(p.written),
|
|
@@ -96,6 +102,74 @@ export function toMigrationStatus(body) {
|
|
|
96
102
|
},
|
|
97
103
|
};
|
|
98
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* GET /api/migration/legacy-rows and its POST remove: a row count, or 0, and
|
|
107
|
+
* after a removal the live samples dropped during the swap, normally 0.
|
|
108
|
+
*/
|
|
109
|
+
export function toLegacyImportRows(body) {
|
|
110
|
+
const count = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
|
|
111
|
+
return {
|
|
112
|
+
rows: count(isRecord(body) ? body.rows : undefined),
|
|
113
|
+
dropped: count(isRecord(body) ? body.dropped : undefined),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* POST /api/migration/contexts: the vessels the source holds. A body without
|
|
118
|
+
* a proper list reads as "none found", which leaves the choice out of the form
|
|
119
|
+
* rather than rendering a broken one.
|
|
120
|
+
*/
|
|
121
|
+
export function toMigrationContexts(body) {
|
|
122
|
+
const contexts = isRecord(body) && Array.isArray(body.contexts)
|
|
123
|
+
? body.contexts.filter((c) => typeof c === "string" && !!c)
|
|
124
|
+
: [];
|
|
125
|
+
const self = isRecord(body) &&
|
|
126
|
+
typeof body.self === "string" &&
|
|
127
|
+
contexts.includes(body.self)
|
|
128
|
+
? body.self
|
|
129
|
+
: undefined;
|
|
130
|
+
return { contexts, self };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* An ISO instant from the server, in the reader's own locale and time zone.
|
|
134
|
+
* Anything that is not a date is shown as it came rather than as
|
|
135
|
+
* "Invalid Date".
|
|
136
|
+
*/
|
|
137
|
+
export function formatDateTime(iso) {
|
|
138
|
+
const d = new Date(iso);
|
|
139
|
+
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The stopped import a status response offers to continue, if any.
|
|
143
|
+
*
|
|
144
|
+
* Everything the notice prints is required: a partial one would render as
|
|
145
|
+
* "import of undefined", so a body missing any of it yields no notice at all.
|
|
146
|
+
*/
|
|
147
|
+
export function toMigrationInterrupted(body) {
|
|
148
|
+
const i = isRecord(body) ? body.interrupted : undefined;
|
|
149
|
+
if (!isRecord(i))
|
|
150
|
+
return undefined;
|
|
151
|
+
if (typeof i.url !== "string" ||
|
|
152
|
+
typeof i.type !== "string" ||
|
|
153
|
+
typeof i.bucket !== "string" ||
|
|
154
|
+
typeof i.from !== "string" ||
|
|
155
|
+
typeof i.to !== "string" ||
|
|
156
|
+
typeof i.updatedAt !== "string")
|
|
157
|
+
return undefined;
|
|
158
|
+
return {
|
|
159
|
+
url: i.url,
|
|
160
|
+
type: i.type,
|
|
161
|
+
bucket: i.bucket,
|
|
162
|
+
from: i.from,
|
|
163
|
+
to: i.to,
|
|
164
|
+
updatedAt: i.updatedAt,
|
|
165
|
+
measurementsDone: typeof i.measurementsDone === "number" &&
|
|
166
|
+
Number.isFinite(i.measurementsDone)
|
|
167
|
+
? i.measurementsDone
|
|
168
|
+
: 0,
|
|
169
|
+
measurement: typeof i.measurement === "string" ? i.measurement : undefined,
|
|
170
|
+
windowStart: typeof i.windowStart === "string" ? i.windowStart : undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
99
173
|
/**
|
|
100
174
|
* Validate the two `datetime-local` values an import range is entered as.
|
|
101
175
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/configpanel/responses.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,wEAAwE;AACxE,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,4EAA4E;
|
|
1
|
+
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/configpanel/responses.ts"],"names":[],"mappings":"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"}
|
package/dist/ilp-writer.d.ts
CHANGED
|
@@ -11,11 +11,16 @@ export declare class ILPWriter {
|
|
|
11
11
|
private connected;
|
|
12
12
|
private connecting;
|
|
13
13
|
private stopped;
|
|
14
|
+
private held;
|
|
15
|
+
private watchingTimestamps;
|
|
16
|
+
private oldestWatched;
|
|
14
17
|
private lastNanos;
|
|
15
18
|
private connectedAt;
|
|
16
19
|
private consecutiveFlaps;
|
|
17
20
|
private droppedLines;
|
|
18
21
|
private totalDroppedLines;
|
|
22
|
+
private totalEnqueuedLines;
|
|
23
|
+
private totalSettledLines;
|
|
19
24
|
private readonly maxBufferLines;
|
|
20
25
|
private unhealthy;
|
|
21
26
|
private awaitingDrain;
|
|
@@ -78,6 +83,53 @@ export declare class ILPWriter {
|
|
|
78
83
|
private requeueFront;
|
|
79
84
|
private enforceBufferCap;
|
|
80
85
|
get droppedLineCount(): number;
|
|
86
|
+
/**
|
|
87
|
+
* Every line ever enqueued, monotonic: a line's place in the sequence. With
|
|
88
|
+
* the two counters below it tells a caller whether what it wrote up to some
|
|
89
|
+
* point has left this process. Lines leave in the order they came, so once
|
|
90
|
+
* `settledLineCount + droppedLineCount` reaches the `enqueuedLineCount` it
|
|
91
|
+
* noted then, nothing enqueued before that point is still waiting here.
|
|
92
|
+
*/
|
|
93
|
+
get enqueuedLineCount(): number;
|
|
94
|
+
/**
|
|
95
|
+
* From now on, note the oldest timestamp of the lines written.
|
|
96
|
+
*
|
|
97
|
+
* A caller that copies a table while the writer keeps writing into it needs
|
|
98
|
+
* to know how far back the rows it must carry over can reach, and only the
|
|
99
|
+
* writer sees the timestamps: a source with a wrong clock records the
|
|
100
|
+
* present under any date it likes.
|
|
101
|
+
*/
|
|
102
|
+
startTimestampWatch(): void;
|
|
103
|
+
/** The oldest timestamp written since the watch began — null if nothing
|
|
104
|
+
* was — and the end of the watch. */
|
|
105
|
+
endTimestampWatch(): bigint | null;
|
|
106
|
+
/**
|
|
107
|
+
* Send what is buffered, then keep further lines back until release().
|
|
108
|
+
*
|
|
109
|
+
* For the seconds a table is swapped out from under the writer: a line
|
|
110
|
+
* arriving while the table is absent would have QuestDB create a fresh one
|
|
111
|
+
* with the wrong schema, and the swap would then fail against it. Flushing
|
|
112
|
+
* first makes the last line written before the hold the last line sent, so
|
|
113
|
+
* a caller that wrote a marker can wait for QuestDB to show it and know
|
|
114
|
+
* everything before it has arrived too. Lines keep being accepted and the
|
|
115
|
+
* buffer cap keeps applying, so a hold must be short.
|
|
116
|
+
*/
|
|
117
|
+
hold(): void;
|
|
118
|
+
release(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Whether the next enqueue would push the cap and discard the oldest line.
|
|
121
|
+
* A bulk writer that must never write behind a gap checks this first: the
|
|
122
|
+
* check and its write run without yielding, so nothing can fill the buffer
|
|
123
|
+
* in between.
|
|
124
|
+
*/
|
|
125
|
+
get atCapacity(): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Lines the socket has accepted, monotonic and exact — counted when the
|
|
128
|
+
* write completes, not when it is issued, and never for a batch that failed
|
|
129
|
+
* and was re-queued. Unlike `pendingLines`, which estimates the socket's
|
|
130
|
+
* share from a byte count and is only good for deciding whether to pause.
|
|
131
|
+
*/
|
|
132
|
+
get settledLineCount(): number;
|
|
81
133
|
private flush;
|
|
82
134
|
private startFlushTimer;
|
|
83
135
|
private stopFlushTimer;
|
package/dist/ilp-writer.js
CHANGED
|
@@ -71,6 +71,9 @@ export class ILPWriter {
|
|
|
71
71
|
connected = false;
|
|
72
72
|
connecting = false;
|
|
73
73
|
stopped = false;
|
|
74
|
+
held = false;
|
|
75
|
+
watchingTimestamps = false;
|
|
76
|
+
oldestWatched = null;
|
|
74
77
|
// Last timestamp handed out, in nanoseconds. Rows are stamped at write time
|
|
75
78
|
// with the server clock (see the delta handler), but `Date` is only
|
|
76
79
|
// millisecond-resolution while the `signalk`/`signalk_str` tables dedup on
|
|
@@ -95,6 +98,8 @@ export class ILPWriter {
|
|
|
95
98
|
// is at least counted.
|
|
96
99
|
droppedLines = 0;
|
|
97
100
|
totalDroppedLines = 0;
|
|
101
|
+
totalEnqueuedLines = 0;
|
|
102
|
+
totalSettledLines = 0;
|
|
98
103
|
maxBufferLines;
|
|
99
104
|
unhealthy = false;
|
|
100
105
|
// True between a backpressured write and the socket's next drain. Guards
|
|
@@ -291,16 +296,16 @@ export class ILPWriter {
|
|
|
291
296
|
// up, so an old timestamp would be ignored anyway) nor push it forward.
|
|
292
297
|
writeAtNanos(path, context, value, tsNanos, source) {
|
|
293
298
|
const sourceTag = source ? `,source=${escapeTag(source)}` : "";
|
|
294
|
-
this.enqueue(`signalk,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag} value=${value} ${tsNanos}\n
|
|
299
|
+
this.enqueue(`signalk,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag} value=${value} ${tsNanos}\n`, tsNanos);
|
|
295
300
|
}
|
|
296
301
|
writeStringAtNanos(path, context, value, tsNanos, kind, source) {
|
|
297
302
|
const sourceTag = source ? `,source=${escapeTag(source)}` : "";
|
|
298
303
|
const kindTag = kind ? `,value_kind=${escapeTag(kind)}` : "";
|
|
299
|
-
this.enqueue(`signalk_str,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag}${kindTag} value_str="${escapeFieldString(value)}" ${tsNanos}\n
|
|
304
|
+
this.enqueue(`signalk_str,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag}${kindTag} value_str="${escapeFieldString(value)}" ${tsNanos}\n`, tsNanos);
|
|
300
305
|
}
|
|
301
306
|
writePositionAtNanos(context, position, tsNanos, source) {
|
|
302
307
|
const sourceTag = source ? `,source=${escapeTag(source)}` : "";
|
|
303
|
-
this.enqueue(`signalk_position,context=${escapeTag(context)}${sourceTag} lat=${position.latitude},lon=${position.longitude} ${tsNanos}\n
|
|
308
|
+
this.enqueue(`signalk_position,context=${escapeTag(context)}${sourceTag} lat=${position.latitude},lon=${position.longitude} ${tsNanos}\n`, tsNanos);
|
|
304
309
|
}
|
|
305
310
|
/**
|
|
306
311
|
* Roughly how much is still in flight, so a bulk importer can apply
|
|
@@ -328,7 +333,7 @@ export class ILPWriter {
|
|
|
328
333
|
write(path, context, value, timestamp, source) {
|
|
329
334
|
const ts = this.nextNanos(timestamp);
|
|
330
335
|
const sourceTag = source ? `,source=${escapeTag(source)}` : "";
|
|
331
|
-
this.enqueue(`signalk,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag} value=${value} ${ts}\n
|
|
336
|
+
this.enqueue(`signalk,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag} value=${value} ${ts}\n`, ts);
|
|
332
337
|
}
|
|
333
338
|
// `kind` records what the value ORIGINALLY was, so a boolean recorded as
|
|
334
339
|
// "true" stays distinguishable from a path whose text value is the word
|
|
@@ -339,17 +344,22 @@ export class ILPWriter {
|
|
|
339
344
|
const ts = this.nextNanos(timestamp);
|
|
340
345
|
const sourceTag = source ? `,source=${escapeTag(source)}` : "";
|
|
341
346
|
const kindTag = kind ? `,value_kind=${escapeTag(kind)}` : "";
|
|
342
|
-
this.enqueue(`signalk_str,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag}${kindTag} value_str="${escapeFieldString(value)}" ${ts}\n
|
|
347
|
+
this.enqueue(`signalk_str,path=${escapeTag(path)},context=${escapeTag(context)}${sourceTag}${kindTag} value_str="${escapeFieldString(value)}" ${ts}\n`, ts);
|
|
343
348
|
}
|
|
344
349
|
// signalk_position has no path column — it holds navigation.position rows
|
|
345
350
|
// exclusively (the caller enforces that), keyed on KEYS(ts, context, source).
|
|
346
351
|
writePosition(context, position, timestamp, source) {
|
|
347
352
|
const ts = this.nextNanos(timestamp);
|
|
348
353
|
const sourceTag = source ? `,source=${escapeTag(source)}` : "";
|
|
349
|
-
this.enqueue(`signalk_position,context=${escapeTag(context)}${sourceTag} lat=${position.latitude},lon=${position.longitude} ${ts}\n
|
|
354
|
+
this.enqueue(`signalk_position,context=${escapeTag(context)}${sourceTag} lat=${position.latitude},lon=${position.longitude} ${ts}\n`, ts);
|
|
350
355
|
}
|
|
351
|
-
enqueue(line) {
|
|
356
|
+
enqueue(line, tsNanos) {
|
|
352
357
|
this.buffer.push(line);
|
|
358
|
+
this.totalEnqueuedLines++;
|
|
359
|
+
if (this.watchingTimestamps &&
|
|
360
|
+
(this.oldestWatched === null || tsNanos < this.oldestWatched)) {
|
|
361
|
+
this.oldestWatched = tsNanos;
|
|
362
|
+
}
|
|
353
363
|
this.enforceBufferCap();
|
|
354
364
|
if (this.buffer.length >= FLUSH_BATCH_SIZE) {
|
|
355
365
|
this.flush();
|
|
@@ -385,7 +395,76 @@ export class ILPWriter {
|
|
|
385
395
|
get droppedLineCount() {
|
|
386
396
|
return this.totalDroppedLines;
|
|
387
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Every line ever enqueued, monotonic: a line's place in the sequence. With
|
|
400
|
+
* the two counters below it tells a caller whether what it wrote up to some
|
|
401
|
+
* point has left this process. Lines leave in the order they came, so once
|
|
402
|
+
* `settledLineCount + droppedLineCount` reaches the `enqueuedLineCount` it
|
|
403
|
+
* noted then, nothing enqueued before that point is still waiting here.
|
|
404
|
+
*/
|
|
405
|
+
get enqueuedLineCount() {
|
|
406
|
+
return this.totalEnqueuedLines;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* From now on, note the oldest timestamp of the lines written.
|
|
410
|
+
*
|
|
411
|
+
* A caller that copies a table while the writer keeps writing into it needs
|
|
412
|
+
* to know how far back the rows it must carry over can reach, and only the
|
|
413
|
+
* writer sees the timestamps: a source with a wrong clock records the
|
|
414
|
+
* present under any date it likes.
|
|
415
|
+
*/
|
|
416
|
+
startTimestampWatch() {
|
|
417
|
+
this.watchingTimestamps = true;
|
|
418
|
+
this.oldestWatched = null;
|
|
419
|
+
}
|
|
420
|
+
/** The oldest timestamp written since the watch began — null if nothing
|
|
421
|
+
* was — and the end of the watch. */
|
|
422
|
+
endTimestampWatch() {
|
|
423
|
+
this.watchingTimestamps = false;
|
|
424
|
+
const oldest = this.oldestWatched;
|
|
425
|
+
this.oldestWatched = null;
|
|
426
|
+
return oldest;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Send what is buffered, then keep further lines back until release().
|
|
430
|
+
*
|
|
431
|
+
* For the seconds a table is swapped out from under the writer: a line
|
|
432
|
+
* arriving while the table is absent would have QuestDB create a fresh one
|
|
433
|
+
* with the wrong schema, and the swap would then fail against it. Flushing
|
|
434
|
+
* first makes the last line written before the hold the last line sent, so
|
|
435
|
+
* a caller that wrote a marker can wait for QuestDB to show it and know
|
|
436
|
+
* everything before it has arrived too. Lines keep being accepted and the
|
|
437
|
+
* buffer cap keeps applying, so a hold must be short.
|
|
438
|
+
*/
|
|
439
|
+
hold() {
|
|
440
|
+
this.flush();
|
|
441
|
+
this.held = true;
|
|
442
|
+
}
|
|
443
|
+
release() {
|
|
444
|
+
this.held = false;
|
|
445
|
+
this.flush();
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Whether the next enqueue would push the cap and discard the oldest line.
|
|
449
|
+
* A bulk writer that must never write behind a gap checks this first: the
|
|
450
|
+
* check and its write run without yielding, so nothing can fill the buffer
|
|
451
|
+
* in between.
|
|
452
|
+
*/
|
|
453
|
+
get atCapacity() {
|
|
454
|
+
return this.buffer.length >= this.maxBufferLines;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Lines the socket has accepted, monotonic and exact — counted when the
|
|
458
|
+
* write completes, not when it is issued, and never for a batch that failed
|
|
459
|
+
* and was re-queued. Unlike `pendingLines`, which estimates the socket's
|
|
460
|
+
* share from a byte count and is only good for deciding whether to pause.
|
|
461
|
+
*/
|
|
462
|
+
get settledLineCount() {
|
|
463
|
+
return this.totalSettledLines;
|
|
464
|
+
}
|
|
388
465
|
flush() {
|
|
466
|
+
if (this.held)
|
|
467
|
+
return;
|
|
389
468
|
if (!this.connected || !this.socket || this.buffer.length === 0)
|
|
390
469
|
return;
|
|
391
470
|
const data = this.buffer.join("");
|
|
@@ -402,7 +481,9 @@ export class ILPWriter {
|
|
|
402
481
|
if (err) {
|
|
403
482
|
this.requeueFront(lines);
|
|
404
483
|
this.debug(`ILP write failed, re-queued batch: ${err.message}`);
|
|
484
|
+
return;
|
|
405
485
|
}
|
|
486
|
+
this.totalSettledLines += lines.length;
|
|
406
487
|
});
|
|
407
488
|
if (!canWrite && !this.awaitingDrain) {
|
|
408
489
|
// ONE listener at a time. `once` still accumulates while the socket
|