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.
Files changed (40) hide show
  1. package/README.md +112 -27
  2. package/dist/api-contract.d.ts +45 -0
  3. package/dist/configpanel/responses.d.ts +28 -1
  4. package/dist/configpanel/responses.js +74 -0
  5. package/dist/configpanel/responses.js.map +1 -1
  6. package/dist/ilp-writer.d.ts +52 -0
  7. package/dist/ilp-writer.js +88 -7
  8. package/dist/ilp-writer.js.map +1 -1
  9. package/dist/index.js +477 -45
  10. package/dist/index.js.map +1 -1
  11. package/dist/migration-checkpoint.d.ts +170 -0
  12. package/dist/migration-checkpoint.js +276 -0
  13. package/dist/migration-checkpoint.js.map +1 -0
  14. package/dist/migration-cleanup.d.ts +48 -0
  15. package/dist/migration-cleanup.js +155 -0
  16. package/dist/migration-cleanup.js.map +1 -0
  17. package/dist/migration.d.ts +134 -17
  18. package/dist/migration.js +694 -158
  19. package/dist/migration.js.map +1 -1
  20. package/dist/questdb-mount.d.ts +106 -0
  21. package/dist/questdb-mount.js +173 -0
  22. package/dist/questdb-mount.js.map +1 -0
  23. package/package.json +2 -2
  24. package/public/assets/AppPanel-p27aQlBS.js +1 -0
  25. package/public/assets/PluginConfigurationPanel-BO_4OxBo.js +9 -0
  26. package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-C2P0b0We.js +1 -0
  27. package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__275086164343916__loadShare__react__loadShare__.js-BWO9MGdR.js +1 -0
  28. package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__275086164343916__prebuild__react__prebuild__-D2g10OtU.js +1 -0
  29. package/public/assets/{dist-BgxWXJ5y.js → dist-DT1ZTQL0.js} +1 -1
  30. package/public/assets/{hostInit-CyS9_M1c.js → hostInit-CkflWBLR.js} +1 -1
  31. package/public/assets/index-CiKXXQrp.js +2 -0
  32. package/public/assets/pendingShares-BI27NR0_.js +1 -0
  33. package/public/assets/{vite-preload-helper-HclGiUj8.js → vite-preload-helper-uBIymjUX.js} +1 -1
  34. package/public/remoteEntry.js +2 -2
  35. package/public/assets/AppPanel-CNFtJpeq.js +0 -1
  36. package/public/assets/PluginConfigurationPanel-CR4mD9IH.js +0 -9
  37. package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-7tb3Q-Bx.js +0 -1
  38. package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__1__loadShare__react__loadShare__.js-DzhF4wXp.js +0 -1
  39. package/public/assets/index-Dh-t5VMR.js +0 -1
  40. 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/start` | Start an import; returns immediately, progress via `/migration/status` |
174
- | GET | `/migration/status` | Progress and state of the current/last import |
175
- | POST | `/migration/cancel` | Cancel the running import |
176
- | GET | `/export?from=...&to=...&format=parquet` | Parquet or CSV export of the `signalk` numeric table (date range required) |
177
- | GET | `/full-export/tables` | List tables exposed by the per-table full-export route |
178
- | GET | `/full-export/:table?from=...&to=...` | Stream a table as Parquet. Optional half-open `[from, to)` range for slicing into shards |
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; a measurement with several named fields becomes
210
- `measurement.field` paths, so two fields cannot overwrite each other.
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`), and `latitude`/`longitude` field pairs are
213
- recombined into `signalk_position`.
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**. An interrupted import can simply be run again.
220
-
221
- Anything that cannot be mapped (a gap, an unsupported value type, a latitude
222
- with no matching longitude) is counted in the run's `skipped` total rather than
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 stored at `~/.signalk/plugin-config-data/signalk-questdb/` on the host, mounted into the container at `/var/lib/questdb`. Data survives container restarts, image upgrades, and plugin disable/enable cycles.
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
 
@@ -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":"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"}
1
+ {"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/configpanel/responses.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -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