signalk-questdb 2.4.0 → 2.5.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 +17 -2
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +80 -0
- package/CLAUDE.md +1 -79
- package/README.md +29 -9
- package/dist/change-gate.d.ts +6 -6
- package/dist/change-gate.js +8 -6
- package/dist/change-gate.js.map +1 -1
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +6 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +38 -13
- package/dist/index.js.map +1 -1
- package/dist/path-matcher.d.ts +5 -4
- package/dist/path-matcher.js +17 -10
- package/dist/path-matcher.js.map +1 -1
- package/package.json +4 -4
- package/public/assets/{AppPanel-C293yEHl.js → AppPanel-D_HLFa10.js} +1 -1
- package/public/assets/PluginConfigurationPanel-CAj6IDmY.js +13 -0
- package/public/assets/{_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-BH9mY6vj.js → _virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-CTTydXPl.js} +1 -1
- package/public/assets/{_virtual_mf___mfe_internal__signalk_questdb__mf_owner__97224418320974__loadShare__shared-H1lSL0nN.js → _virtual_mf___mfe_internal__signalk_questdb__mf_owner__97224418320974__loadShare__shared-BgRwx7LZ.js} +1 -1
- package/public/assets/{dist-BhVCbinB.js → dist-B4My1YVh.js} +1 -1
- package/public/assets/helpers-PTLizuYm.js +1 -0
- package/public/assets/hostInit-Dvn0aCSd.js +1 -0
- package/public/assets/{index-QJC86Uvi.js → index-j5iNctbW.js} +2 -2
- package/public/assets/pendingShares-BHVf8sM9.js +2 -0
- package/public/assets/{utils-D62kKhzq.js → utils-BesU7hIa.js} +1 -1
- package/public/assets/{vite-preload-helper-CRzLMJPS.js → vite-preload-helper-DclECHGd.js} +1 -1
- package/public/remoteEntry.js +2 -2
- package/public/assets/PluginConfigurationPanel-7qfy8fIR.js +0 -11
- package/public/assets/helpers-CvnX61Ju.js +0 -1
- package/public/assets/hostInit-Ds8KD_fS.js +0 -1
- package/public/assets/pendingShares-DemvETkM.js +0 -2
package/.coderabbit.yaml
CHANGED
|
@@ -136,7 +136,7 @@ reviews:
|
|
|
136
136
|
must narrow `unknown` and avoid `any`.
|
|
137
137
|
- The configpanel is TypeScript too, with its own `tsconfig.json`
|
|
138
138
|
(browser libs, `noEmit`), checked by `npm run typecheck:panel`.
|
|
139
|
-
- Node `>=
|
|
139
|
+
- Node `>= 24` per `engines` (uses Node's built-in test runner).
|
|
140
140
|
|
|
141
141
|
## Tests
|
|
142
142
|
|
|
@@ -145,7 +145,13 @@ reviews:
|
|
|
145
145
|
(`npm run build:all`). Flag changes that add tests but don't
|
|
146
146
|
match the existing pattern.
|
|
147
147
|
|
|
148
|
-
##
|
|
148
|
+
## Comments
|
|
149
|
+
|
|
150
|
+
Code explains itself: names, types and small functions say what
|
|
151
|
+
it does. Do not ask for docstrings or JSDoc, and do not flag a
|
|
152
|
+
function for lacking one. A comment is there for the *why* the
|
|
153
|
+
code cannot show: a constraint, a trap, a choice against the
|
|
154
|
+
obvious alternative.
|
|
149
155
|
|
|
150
156
|
Flag any comment that merely restates what the code already says.
|
|
151
157
|
Examples to flag:
|
|
@@ -222,3 +228,12 @@ reviews:
|
|
|
222
228
|
`ContainerResourceLimits` (memory + cpus) — the defaults of
|
|
223
229
|
`768m` / `1.5 cpus` are deliberate for the typical Signal K
|
|
224
230
|
host stack (questdb + grafana + mayara + backup co-resident).
|
|
231
|
+
|
|
232
|
+
# Docstrings are not wanted for their own sake; see "Comments" above.
|
|
233
|
+
finishing_touches:
|
|
234
|
+
docstrings:
|
|
235
|
+
enabled: false
|
|
236
|
+
|
|
237
|
+
pre_merge_checks:
|
|
238
|
+
docstrings:
|
|
239
|
+
mode: "off"
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents working in this repository. `CLAUDE.md` imports it.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `npm run build` — TypeScript compile (`tsc`) + panel typecheck + Vite bundle of the React config panel into `public/`
|
|
8
|
+
- `npm run build:config` — Vite only (rebuild the config panel without recompiling TS)
|
|
9
|
+
- `npm run typecheck:panel` — typecheck the config panel (`tsc -p src/configpanel --noEmit`); Vite only strips its types, so this is the only thing that checks them
|
|
10
|
+
- `npm run watch` — `tsc --watch` for the plugin source
|
|
11
|
+
- `npm test` — runs the Node built-in test runner against compiled output (`node --test 'dist/test/**/*.test.js'`). Tests must be built first; use `npm run build:all` to build then test.
|
|
12
|
+
- `npm run format` — prettier write + eslint --fix
|
|
13
|
+
- `npm run ci-lint` — eslint + prettier --check (used in CI)
|
|
14
|
+
- Run a single test file: `node --test dist/test/history-v2.test.js` (after `npm run build`)
|
|
15
|
+
|
|
16
|
+
Node ≥ 24 is required (uses the built-in test runner).
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
This is a Signal K **server plugin** that ingests vessel data into a managed **QuestDB** container and serves it back via Signal K's history APIs. It is intended as a drop-in replacement for `signalk-to-influxdb`.
|
|
21
|
+
|
|
22
|
+
### Two surfaces
|
|
23
|
+
|
|
24
|
+
1. **Plugin runtime** (`src/index.ts` and siblings) — compiled by `tsc` to `dist/`, loaded by Signal K server as `main` (`dist/index.js`). The package is **ESM** (`"type": "module"`): `src/index.ts` ends in `export default`, and Signal K's `importOrRequire()` unwraps it. Node ≥ 20.19 can `require()` an ESM module, which every Node version the plugin supports does.
|
|
25
|
+
2. **React config panel** (`src/configpanel/`) — TypeScript + JSX, bundled by **Vite** (`vite.config.ts`) via **Module Federation** into `public/remoteEntry.js`, exposed to the Signal K Admin UI as `./PluginConfigurationPanel`. React 19 is shared as a singleton with the host UI. It has its own `tsconfig.json` (browser libs, classic JSX, `noEmit`) so DOM types never reach the Node compile.
|
|
26
|
+
|
|
27
|
+
Because the package is `"type": "module"`, the server injects the panel's script tag with `type="module"`, and the Admin UI then loads it as an **ESM remote**: `await import(remoteEntry.js)` expecting exported `get`/`init` (see the admin UI's `views/Webapps/dynamicutilities.ts`). Vite's federation output provides exactly that.
|
|
28
|
+
|
|
29
|
+
Both build outputs are shipped in the npm package; `prepublishOnly` rebuilds them.
|
|
30
|
+
|
|
31
|
+
### Data flow
|
|
32
|
+
|
|
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. The archives `signalk_archive` and `signalk_position_archive` are defined there too, with their base tables' columns and dedup keys and monthly partitions.
|
|
35
|
+
- Reads come from two registered providers:
|
|
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). Where the archive covers the range, each table is sampled on its own and the buckets merged in the plugin.
|
|
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. 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, and v2's sampled and raw reads of the own vessel read each table on its own and merge the buckets or rows in the plugin. Their union (`readFrom` in `src/archive.ts`) sorts every row in range, so only reads that need one order across both tables, such as `middle_index`, or that a short window bounds still use it.
|
|
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.
|
|
40
|
+
- Startup restore (`src/restore.ts`) optionally replays each vessel's last recorded position back into the live model after a server restart.
|
|
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.
|
|
42
|
+
|
|
43
|
+
### Container integration
|
|
44
|
+
|
|
45
|
+
The plugin **does not run QuestDB itself**. It names `signalk-container` in `package.json`'s `signalk.requires`, with no npm dependency on it, and looks up that plugin's API at runtime (`ContainerManagerApi` in `src/index.ts`). When `managedContainer` is true, it calls `ensureRunning` / `ensureNetwork` / `pullImage` / `stop` to control a QuestDB container on the shared `sk-network` Podman/Docker network. The plugin lifecycle (`start`/`stop`) drives container lifecycle.
|
|
46
|
+
|
|
47
|
+
When `managedContainer` is false, the plugin connects to an external QuestDB at the configured host/ports.
|
|
48
|
+
|
|
49
|
+
### Config schema
|
|
50
|
+
|
|
51
|
+
`src/config/schema.ts` uses **TypeBox** — the schema is the single source of truth for the JSON-schema Signal K shows in the Admin UI **and** for the `Config` TS type. Add new options there.
|
|
52
|
+
|
|
53
|
+
The package is the unscoped `typebox` (1.x), not `@sinclair/typebox` (which stopped at 0.34). TypeBox 1 was published under a new name rather than a major bump, so `npm outdated` will never point at it.
|
|
54
|
+
|
|
55
|
+
### REST endpoints
|
|
56
|
+
|
|
57
|
+
All extra plugin endpoints live under `/plugins/signalk-questdb/api/` and are wired in `src/index.ts` via the `IRouter` Signal K passes to `registerWithRouter`. The `/query` endpoint is gated by `isReadOnlySQL` in `src/query-client.ts` — DDL/DML must remain blocked.
|
|
58
|
+
|
|
59
|
+
⚠️ **`/console` is deliberately NOT gated that way.** It proxies QuestDB's own console, which is a full SQL client and can drop tables. It is safe only because signalk-server gives plugin routes an **admin-only default**, and routes are downgraded to readwrite/readonly _only_ by registering them through `router.access(...)`. Never wrap the console mount in `.access()` — doing so hands non-admins the ability to delete recorded history. The mount is also gated on the `enableConsole` config flag, so turning it off removes the route rather than leaving it to refuse.
|
|
60
|
+
|
|
61
|
+
Their wire shapes live in `src/api-contract.ts` and are shared by both surfaces: handlers assert responses with `satisfies`, and the panel casts fetch results to the same types. Add or rename a response field there, not inline — that is what stops the server and panel drifting apart silently.
|
|
62
|
+
|
|
63
|
+
### Tests
|
|
64
|
+
|
|
65
|
+
`src/test/` contains `node:test` suites for ILP encoding, time range parsing, query building, and v2 history. They run against the **compiled** JS in `dist/test/`, so always build before testing.
|
|
66
|
+
|
|
67
|
+
`src/test/history-live.test.ts` runs the v2 provider against a real QuestDB. It drops the plugin's tables, so run it only against a throwaway instance; its header says how.
|
|
68
|
+
|
|
69
|
+
## Conventions
|
|
70
|
+
|
|
71
|
+
- TypeScript strict mode everywhere, panel included; do not loosen either `tsconfig.json`. In particular never add `jsx` or a DOM `lib` to the root one — server code referencing `document` would then typecheck clean.
|
|
72
|
+
- **Relative imports in the runtime need the `.js` extension** (`./query-client.js`, even though the source is `.ts`). That is Node's ESM rule, enforced by `moduleResolution: "nodenext"`. Omitting it fails the build, which is the point — switching to `"bundler"` resolution would silently emit unresolvable specifiers instead.
|
|
73
|
+
- **`.npmignore` excludes config files by exact filename** — rename one and the stale entry silently publishes it. `eslint.config.ts` is loaded through `jiti` (eslint 10 needs it for TypeScript configs); it is a devDependency and never ships.
|
|
74
|
+
- **The three ignore files serve opposite purposes for build output.** `public/assets/` is gitignored and prettier-ignored, but must NEVER be added to `.npmignore`: `remoteEntry.js` imports those chunks by name, so excluding them publishes a panel that 404s on load. Only genuinely local artifacts (`.mf/`) belong in all three.
|
|
75
|
+
- Prettier + eslint flat config (`eslint.config.ts`); run `npm run format` before committing.
|
|
76
|
+
- The config panel uses React 19 with Module Federation — keep the federation `shared` block in sync with `package.json`'s React version.
|
|
77
|
+
- **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.
|
|
78
|
+
- **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.
|
|
79
|
+
- `signalk.appIcon` and `signalk.displayName` in `package.json` control how the plugin appears in the Admin UI.
|
|
80
|
+
- **Code explains itself.** Names, types and small functions say what the code does; a comment says why — a constraint, a trap, a choice against the obvious alternative. Don't write docstrings for their own sake or comments that restate the code. CodeRabbit's docstring check is off for the same reason.
|
package/CLAUDE.md
CHANGED
|
@@ -1,79 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
- `npm run build` — TypeScript compile (`tsc`) + panel typecheck + Vite bundle of the React config panel into `public/`
|
|
8
|
-
- `npm run build:config` — Vite only (rebuild the config panel without recompiling TS)
|
|
9
|
-
- `npm run typecheck:panel` — typecheck the config panel (`tsc -p src/configpanel --noEmit`); Vite only strips its types, so this is the only thing that checks them
|
|
10
|
-
- `npm run watch` — `tsc --watch` for the plugin source
|
|
11
|
-
- `npm test` — runs the Node built-in test runner against compiled output (`node --test 'dist/test/**/*.test.js'`). Tests must be built first; use `npm run build:all` to build then test.
|
|
12
|
-
- `npm run format` — prettier write + eslint --fix
|
|
13
|
-
- `npm run ci-lint` — eslint + prettier --check (used in CI)
|
|
14
|
-
- Run a single test file: `node --test dist/test/history-v2.test.js` (after `npm run build`)
|
|
15
|
-
|
|
16
|
-
Node ≥ 22 is required (uses the built-in test runner).
|
|
17
|
-
|
|
18
|
-
## Architecture
|
|
19
|
-
|
|
20
|
-
This is a Signal K **server plugin** that ingests vessel data into a managed **QuestDB** container and serves it back via Signal K's history APIs. It is intended as a drop-in replacement for `signalk-to-influxdb`.
|
|
21
|
-
|
|
22
|
-
### Two surfaces
|
|
23
|
-
|
|
24
|
-
1. **Plugin runtime** (`src/index.ts` and siblings) — compiled by `tsc` to `dist/`, loaded by Signal K server as `main` (`dist/index.js`). The package is **ESM** (`"type": "module"`): `src/index.ts` ends in `export default`, and Signal K's `importOrRequire()` unwraps it. Node ≥ 20.19 can `require()` an ESM module, so this works on both supported Node versions.
|
|
25
|
-
2. **React config panel** (`src/configpanel/`) — TypeScript + JSX, bundled by **Vite** (`vite.config.ts`) via **Module Federation** into `public/remoteEntry.js`, exposed to the Signal K Admin UI as `./PluginConfigurationPanel`. React 19 is shared as a singleton with the host UI. It has its own `tsconfig.json` (browser libs, classic JSX, `noEmit`) so DOM types never reach the Node compile.
|
|
26
|
-
|
|
27
|
-
Because the package is `"type": "module"`, the server injects the panel's script tag with `type="module"`, and the Admin UI then loads it as an **ESM remote**: `await import(remoteEntry.js)` expecting exported `get`/`init` (see the admin UI's `views/Webapps/dynamicutilities.ts`). Vite's federation output provides exactly that.
|
|
28
|
-
|
|
29
|
-
Both build outputs are shipped in the npm package; `prepublishOnly` rebuilds them.
|
|
30
|
-
|
|
31
|
-
### Data flow
|
|
32
|
-
|
|
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. The archives `signalk_archive` and `signalk_position_archive` are defined there too, with their base tables' columns and dedup keys and monthly partitions.
|
|
35
|
-
- Reads come from two registered providers:
|
|
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). Where the archive covers the range, each table is sampled on its own and the buckets merged in the plugin.
|
|
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. 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, and v2's sampled and raw reads of the own vessel read each table on its own and merge the buckets or rows in the plugin. Their union (`readFrom` in `src/archive.ts`) sorts every row in range, so only reads that need one order across both tables, such as `middle_index`, or that a short window bounds still use it.
|
|
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.
|
|
40
|
-
- Startup restore (`src/restore.ts`) optionally replays each vessel's last recorded position back into the live model after a server restart.
|
|
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.
|
|
42
|
-
|
|
43
|
-
### Container integration
|
|
44
|
-
|
|
45
|
-
The plugin **does not run QuestDB itself**. It declares an optional peer dep on `signalk-container` and looks up that plugin's API at runtime (`ContainerManagerApi` in `src/index.ts`). When `managedContainer` is true, it calls `ensureRunning` / `ensureNetwork` / `pullImage` / `stop` to control a QuestDB container on the shared `sk-network` Podman/Docker network. The plugin lifecycle (`start`/`stop`) drives container lifecycle.
|
|
46
|
-
|
|
47
|
-
When `managedContainer` is false, the plugin connects to an external QuestDB at the configured host/ports.
|
|
48
|
-
|
|
49
|
-
### Config schema
|
|
50
|
-
|
|
51
|
-
`src/config/schema.ts` uses **TypeBox** — the schema is the single source of truth for the JSON-schema Signal K shows in the Admin UI **and** for the `Config` TS type. Add new options there.
|
|
52
|
-
|
|
53
|
-
The package is the unscoped `typebox` (1.x), not `@sinclair/typebox` (which stopped at 0.34). TypeBox 1 was published under a new name rather than a major bump, so `npm outdated` will never point at it.
|
|
54
|
-
|
|
55
|
-
### REST endpoints
|
|
56
|
-
|
|
57
|
-
All extra plugin endpoints live under `/plugins/signalk-questdb/api/` and are wired in `src/index.ts` via the `IRouter` Signal K passes to `registerWithRouter`. The `/query` endpoint is gated by `isReadOnlySQL` in `src/query-client.ts` — DDL/DML must remain blocked.
|
|
58
|
-
|
|
59
|
-
⚠️ **`/console` is deliberately NOT gated that way.** It proxies QuestDB's own console, which is a full SQL client and can drop tables. It is safe only because signalk-server gives plugin routes an **admin-only default**, and routes are downgraded to readwrite/readonly _only_ by registering them through `router.access(...)`. Never wrap the console mount in `.access()` — doing so hands non-admins the ability to delete recorded history. The mount is also gated on the `enableConsole` config flag, so turning it off removes the route rather than leaving it to refuse.
|
|
60
|
-
|
|
61
|
-
Their wire shapes live in `src/api-contract.ts` and are shared by both surfaces: handlers assert responses with `satisfies`, and the panel casts fetch results to the same types. Add or rename a response field there, not inline — that is what stops the server and panel drifting apart silently.
|
|
62
|
-
|
|
63
|
-
### Tests
|
|
64
|
-
|
|
65
|
-
`src/test/` contains `node:test` suites for ILP encoding, time range parsing, query building, and v2 history. They run against the **compiled** JS in `dist/test/`, so always build before testing.
|
|
66
|
-
|
|
67
|
-
`src/test/history-live.test.ts` runs the v2 provider against a real QuestDB. It drops the plugin's tables, so run it only against a throwaway instance; its header says how.
|
|
68
|
-
|
|
69
|
-
## Conventions
|
|
70
|
-
|
|
71
|
-
- TypeScript strict mode everywhere, panel included; do not loosen either `tsconfig.json`. In particular never add `jsx` or a DOM `lib` to the root one — server code referencing `document` would then typecheck clean.
|
|
72
|
-
- **Relative imports in the runtime need the `.js` extension** (`./query-client.js`, even though the source is `.ts`). That is Node's ESM rule, enforced by `moduleResolution: "nodenext"`. Omitting it fails the build, which is the point — switching to `"bundler"` resolution would silently emit unresolvable specifiers instead.
|
|
73
|
-
- **`.npmignore` excludes config files by exact filename** — rename one and the stale entry silently publishes it. `eslint.config.ts` is loaded through `jiti` (eslint 10 needs it for TypeScript configs); it is a devDependency and never ships.
|
|
74
|
-
- **The three ignore files serve opposite purposes for build output.** `public/assets/` is gitignored and prettier-ignored, but must NEVER be added to `.npmignore`: `remoteEntry.js` imports those chunks by name, so excluding them publishes a panel that 404s on load. Only genuinely local artifacts (`.mf/`) belong in all three.
|
|
75
|
-
- Prettier + eslint flat config (`eslint.config.ts`); run `npm run format` before committing.
|
|
76
|
-
- The config panel uses React 19 with Module Federation — keep the federation `shared` block in sync with `package.json`'s React version.
|
|
77
|
-
- **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.
|
|
78
|
-
- **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.
|
|
79
|
-
- `signalk.appIcon` and `signalk.displayName` in `package.json` control how the plugin appears in the Admin UI.
|
|
1
|
+
@AGENTS.md
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ InfluxDB history can be imported.
|
|
|
43
43
|
|
|
44
44
|
### Requirements
|
|
45
45
|
|
|
46
|
-
- Node.js
|
|
46
|
+
- Node.js 24 or newer
|
|
47
47
|
- QuestDB 8.1 or newer (resolution queries use `SAMPLE BY … FROM … TO`). This
|
|
48
48
|
applies to an external QuestDB and to a pinned **QuestDB image version** in
|
|
49
49
|
managed mode; `latest` always qualifies
|
|
@@ -112,11 +112,12 @@ rootless Podman.
|
|
|
112
112
|
| QuestDB memory limit | `768m` | Hard cgroup cap on the container (empty = unlimited); the JVM heap gets half of it, so heap plus off-heap stays inside |
|
|
113
113
|
| QuestDB CPU limit (cores) | `1.5` | Fractional cores (0 = unlimited) |
|
|
114
114
|
| Default sampling rate (ms) | `2000` | Minimum ms between writes for any one path (0 = every update) |
|
|
115
|
-
| Per-path sampling rates (ms) | _(empty)_ | Glob → ms overrides,
|
|
115
|
+
| Per-path sampling rates (ms) | _(empty)_ | Glob → ms overrides, one `pattern ms` per line in the panel (0 = wire speed, see Sampling) |
|
|
116
116
|
| Unchanged-value heartbeat (s) | `300` | A value equal to the last one recorded is written again only this often; changes are written at the sampling rate (0 = every sample, see Change-only recording) |
|
|
117
117
|
| Write batch interval (ms) | `5000` | How often buffered samples are committed — one WAL transaction per table per commit (see Performance) |
|
|
118
118
|
| Record own vessel | `true` | Record the `self` context |
|
|
119
119
|
| Record AIS targets | `true` | Record other vessels |
|
|
120
|
+
| Record all sources | `false` | Record every source of a path, not only the one the source priorities prefer (see Source priorities) |
|
|
120
121
|
| Restore vessels on startup | `false` | Replay each vessel's last recorded position after a restart (see Startup restore) |
|
|
121
122
|
| Restore max age (minutes) | `9` | Only replay values recorded within this window |
|
|
122
123
|
| Allow `sourcePolicy=all` | `false` | Let History API callers split a path into one column per recording source (see History API) |
|
|
@@ -236,9 +237,27 @@ canboat WASM decoder do not produce the marker at all.
|
|
|
236
237
|
|
|
237
238
|
**Sampling.** Each path is written at most once per **Default sampling rate**
|
|
238
239
|
(2000 ms), with per-path overrides by glob for the few paths that need finer
|
|
239
|
-
resolution and the many that need less.
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
resolution and the many that need less. The first value after a window opens
|
|
241
|
+
is written and the rest of the window is dropped, not averaged, so a gust
|
|
242
|
+
between two samples is not in the history. A rate of 0, as default or per
|
|
243
|
+
path, removes the window: `environment.wind.* 0` records every change of the
|
|
244
|
+
wind at wire speed while everything else stays at the default. An unchanged
|
|
245
|
+
value is still written only once per **Unchanged-value heartbeat** (see
|
|
246
|
+
Change-only recording), except `navigation.position`, which is written even
|
|
247
|
+
when unchanged; set the heartbeat to 0 as well to record every update. Wire
|
|
248
|
+
speed costs rows: a wind sensor at 10 Hz writes about 1.7 million a day for
|
|
249
|
+
angle and speed, and every path of a busy NMEA 2000 boat at wire speed can be
|
|
250
|
+
a few hundred rows a second. **Record own vessel** and **Record AIS targets**
|
|
251
|
+
switch the two kinds of context independently, and **Path patterns** exclude
|
|
252
|
+
(or include only) paths by glob.
|
|
253
|
+
|
|
254
|
+
**Source priorities.** When Signal K's source priorities rank the sources of a
|
|
255
|
+
path, only the preferred source is recorded, the same value instruments show.
|
|
256
|
+
Paths without a priority are recorded from every source. **Record all sources**
|
|
257
|
+
records the sources a priority hides as well, for calibrating a sensor or
|
|
258
|
+
comparing two GPS receivers. Each source is then sampled on its own, so a path
|
|
259
|
+
with three sources writes up to three times the rows; with the option off, the
|
|
260
|
+
sources of a path share one sampling window.
|
|
242
261
|
|
|
243
262
|
**Change-only recording.** Most paths on a boat do not change from one sample
|
|
244
263
|
to the next: a switch that flips twice a day, an energy counter at rest, a tank
|
|
@@ -270,8 +289,9 @@ data really stopped:
|
|
|
270
289
|
age**. Keep the heartbeat below that, or a vessel lying still may come back
|
|
271
290
|
with its position but without its speed, course and heading.
|
|
272
291
|
|
|
273
|
-
`navigation.position` is
|
|
274
|
-
sample.
|
|
292
|
+
`navigation.position` is written even when unchanged. A heartbeat of 0 does the
|
|
293
|
+
same for every path: each sample the sampling rate lets through is written. With
|
|
294
|
+
a sampling rate of 0 as well, that is every update.
|
|
275
295
|
|
|
276
296
|
**Batching.** Samples are buffered and committed every **Write batch interval**
|
|
277
297
|
(5 s), one WAL transaction per table per commit. The buffer holds 100 000 lines;
|
|
@@ -900,8 +920,8 @@ QuestDB releases they held memory.
|
|
|
900
920
|
|
|
901
921
|
- **Default sampling rate** of 2000 ms limits each path to one write per two
|
|
902
922
|
seconds, keeping write volume modest on busy NMEA 2000 buses; per-path
|
|
903
|
-
overrides allow faster rates for critical paths
|
|
904
|
-
|
|
923
|
+
overrides allow faster rates, or wire speed with 0, for critical paths while
|
|
924
|
+
slow-changing ones stay throttled.
|
|
905
925
|
- **Change-only recording** skips a value that has not changed since it was last
|
|
906
926
|
written until the heartbeat is due. Paths that hold steady — which is
|
|
907
927
|
most of them — cost one row per heartbeat instead of one per sampling
|
package/dist/change-gate.d.ts
CHANGED
|
@@ -14,13 +14,13 @@ export declare class ChangeGate {
|
|
|
14
14
|
/** Records that `value` was written, so later equal values are repeats. */
|
|
15
15
|
wrote(path: string, context: string, source: string | undefined, value: unknown, now: number): void;
|
|
16
16
|
/**
|
|
17
|
-
* Forgets a path in a context, for every source
|
|
18
|
-
* was flattened into. Signal K reports a
|
|
19
|
-
* without this, the reading that comes
|
|
20
|
-
*
|
|
21
|
-
* held straight through it.
|
|
17
|
+
* Forgets a path in a context, for every source or only for `source` when
|
|
18
|
+
* given, along with any leaves it was flattened into. Signal K reports a
|
|
19
|
+
* path going stale as a null value; without this, the reading that comes
|
|
20
|
+
* back after the gap would be skipped whenever it equals the one before
|
|
21
|
+
* it, and the gap would read as a value held straight through it.
|
|
22
22
|
*/
|
|
23
|
-
forget(path: string, context: string): void;
|
|
23
|
+
forget(path: string, context: string, source?: string): void;
|
|
24
24
|
clear(): void;
|
|
25
25
|
private evict;
|
|
26
26
|
}
|
package/dist/change-gate.js
CHANGED
|
@@ -68,18 +68,20 @@ export class ChangeGate {
|
|
|
68
68
|
paths.set(k, { path, value, at: now });
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Forgets a path in a context, for every source
|
|
72
|
-
* was flattened into. Signal K reports a
|
|
73
|
-
* without this, the reading that comes
|
|
74
|
-
*
|
|
75
|
-
* held straight through it.
|
|
71
|
+
* Forgets a path in a context, for every source or only for `source` when
|
|
72
|
+
* given, along with any leaves it was flattened into. Signal K reports a
|
|
73
|
+
* path going stale as a null value; without this, the reading that comes
|
|
74
|
+
* back after the gap would be skipped whenever it equals the one before
|
|
75
|
+
* it, and the gap would read as a value held straight through it.
|
|
76
76
|
*/
|
|
77
|
-
forget(path, context) {
|
|
77
|
+
forget(path, context, source) {
|
|
78
78
|
const paths = this.byContext.get(context);
|
|
79
79
|
if (!paths)
|
|
80
80
|
return;
|
|
81
81
|
const leafPrefix = `${path}.`;
|
|
82
82
|
for (const [k, entry] of paths) {
|
|
83
|
+
if (source !== undefined && k !== key(entry.path, source))
|
|
84
|
+
continue;
|
|
83
85
|
if (entry.path === path || entry.path.startsWith(leafPrefix)) {
|
|
84
86
|
paths.delete(k);
|
|
85
87
|
this.count -= 1;
|
package/dist/change-gate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-gate.js","sourceRoot":"","sources":["../src/change-gate.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,4EAA4E;AAC5E,uEAAuE;AACvE,0EAA0E;AAC1E,4EAA4E;AAC5E,sEAAsE;AACtE,0EAA0E;AAC1E,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,2DAA2D;AAQ3D,MAAM,OAAO,UAAU;IAYF;IACA;IAZnB,0EAA0E;IAC1E,yEAAyE;IACxD,SAAS,GAAG,IAAI,GAAG,EAAgC,CAAC;IAC7D,KAAK,GAAG,CAAC,CAAC;IAElB,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,gBAAgB;IAChB,YACmB,WAAmB,EACnB,aAAa,MAAM;QADnB,gBAAW,GAAX,WAAW,CAAQ;QACnB,eAAU,GAAV,UAAU,CAAS;IACnC,CAAC;IAEJ,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;;;OAIG;IACH,QAAQ,CACN,IAAY,EACZ,OAAe,EACf,MAA0B,EAC1B,KAAc,EACd,GAAW;QAEX,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAClE,OAAO,CACL,KAAK,KAAK,SAAS;YACnB,KAAK,CAAC,KAAK,KAAK,KAAK;YACrB,GAAG,GAAG,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,WAAW,CAClC,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,KAAK,CACH,IAAY,EACZ,OAAe,EACf,MAA0B,EAC1B,KAAc,EACd,GAAW;QAEX,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC;YAAE,OAAO;QAClC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC5B,IAAI,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBAClC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAChB,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACtC,CAAC;YACD,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;gBAClB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACrC,CAAC;YACD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,IAAY,EAAE,OAAe;
|
|
1
|
+
{"version":3,"file":"change-gate.js","sourceRoot":"","sources":["../src/change-gate.ts"],"names":[],"mappings":"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"}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare const ConfigSchema: Type.TObject<{
|
|
|
18
18
|
unchangedHeartbeatSeconds: Type.TNumber;
|
|
19
19
|
recordSelf: Type.TBoolean;
|
|
20
20
|
recordOthers: Type.TBoolean;
|
|
21
|
+
recordAllSources: Type.TBoolean;
|
|
21
22
|
restoreOnStart: Type.TBoolean;
|
|
22
23
|
restoreMaxAgeMinutes: Type.TNumber;
|
|
23
24
|
historySourcePolicyAll: Type.TBoolean;
|
package/dist/config/schema.js
CHANGED
|
@@ -65,7 +65,7 @@ export const ConfigSchema = Type.Object({
|
|
|
65
65
|
samplingRates: Type.Record(Type.String(), Type.Number(), {
|
|
66
66
|
default: {},
|
|
67
67
|
title: "Per-path sampling rates (ms)",
|
|
68
|
-
description: 'Override default rate for specific paths. e.g. { "environment.wind.*":
|
|
68
|
+
description: 'Override default rate for specific paths (0 = wire speed: every change, while an unchanged value still waits for the heartbeat; navigation.position is written even when unchanged, so 0 writes every position update). e.g. { "environment.wind.*": 0, "tanks.*": 10000 }',
|
|
69
69
|
}),
|
|
70
70
|
unchangedHeartbeatSeconds: Type.Number({
|
|
71
71
|
default: 300,
|
|
@@ -81,6 +81,11 @@ export const ConfigSchema = Type.Object({
|
|
|
81
81
|
default: true,
|
|
82
82
|
title: "Record AIS targets",
|
|
83
83
|
}),
|
|
84
|
+
recordAllSources: Type.Boolean({
|
|
85
|
+
default: false,
|
|
86
|
+
title: "Record all sources",
|
|
87
|
+
description: "Record every source of a path, not only the one Signal K's source priorities prefer. Off by default: priorities pick the better sensor, and its value is what history normally wants. Turn it on to keep the readings a priority hides, e.g. to calibrate a sensor or compare two GPS receivers. Each source is then sampled on its own, so a path with three sources writes up to three times the rows.",
|
|
88
|
+
}),
|
|
84
89
|
restoreOnStart: Type.Boolean({
|
|
85
90
|
default: false,
|
|
86
91
|
title: "Restore vessels on startup",
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ interface App {
|
|
|
13
13
|
getBus: (path?: string) => {
|
|
14
14
|
onValue: (cb: (delta: unknown) => void) => () => void;
|
|
15
15
|
};
|
|
16
|
+
getUnfilteredBus: (path?: string) => {
|
|
17
|
+
onValue: (cb: (delta: unknown) => void) => () => void;
|
|
18
|
+
};
|
|
16
19
|
};
|
|
17
20
|
registerHistoryProvider: (provider: unknown) => void;
|
|
18
21
|
registerHistoryApiProvider: (provider: HistoryApi.HistoryProvider) => void;
|
|
@@ -46,6 +49,7 @@ declare const _default: (app: App) => {
|
|
|
46
49
|
unchangedHeartbeatSeconds: import("typebox").TNumber;
|
|
47
50
|
recordSelf: import("typebox").TBoolean;
|
|
48
51
|
recordOthers: import("typebox").TBoolean;
|
|
52
|
+
recordAllSources: import("typebox").TBoolean;
|
|
49
53
|
restoreOnStart: import("typebox").TBoolean;
|
|
50
54
|
restoreMaxAgeMinutes: import("typebox").TNumber;
|
|
51
55
|
historySourcePolicyAll: import("typebox").TBoolean;
|
package/dist/index.js
CHANGED
|
@@ -556,10 +556,10 @@ export default (app) => {
|
|
|
556
556
|
const matches = matcher.matches(path);
|
|
557
557
|
return mode === "exclude" ? !matches : matches;
|
|
558
558
|
}
|
|
559
|
-
function isThrottled(path, context, defaultRate) {
|
|
559
|
+
function isThrottled(path, context, defaultRate, source) {
|
|
560
560
|
// Per-path override wins when one matches; otherwise the default rate.
|
|
561
561
|
const overrideRate = rateMatcher?.rateFor(path) ?? null;
|
|
562
|
-
return throttle.shouldDrop(path, context, overrideRate ?? defaultRate, Date.now());
|
|
562
|
+
return throttle.shouldDrop(path, context, overrideRate ?? defaultRate, Date.now(), source);
|
|
563
563
|
}
|
|
564
564
|
const WAL_NOTIFICATION_PATH = "notifications.signalk-questdb.walSuspended";
|
|
565
565
|
// Dedupe key of the last emitted notification. The monitor reports every
|
|
@@ -1193,7 +1193,14 @@ export default (app) => {
|
|
|
1193
1193
|
// restore sees every vessel that transmitted during startup.
|
|
1194
1194
|
liveContexts.clear();
|
|
1195
1195
|
trackLiveContexts = config.restoreOnStart;
|
|
1196
|
-
|
|
1196
|
+
// getBus() carries only the source that Signal K's source priorities
|
|
1197
|
+
// prefer on each path, the value instruments show. recordAllSources
|
|
1198
|
+
// reads the unfiltered bus instead, so a source that loses the ranking
|
|
1199
|
+
// is recorded as well: the raw paddlewheel next to a corrected STW
|
|
1200
|
+
// (issue #235).
|
|
1201
|
+
const bus = config.recordAllSources
|
|
1202
|
+
? app.streambundle.getUnfilteredBus()
|
|
1203
|
+
: app.streambundle.getBus();
|
|
1197
1204
|
const unsub = bus.onValue((delta) => {
|
|
1198
1205
|
if (!writer)
|
|
1199
1206
|
return;
|
|
@@ -1212,9 +1219,26 @@ export default (app) => {
|
|
|
1212
1219
|
// syntax. Undefined when the delta carries none — the column stays
|
|
1213
1220
|
// null, same as rows written before it existed.
|
|
1214
1221
|
const source = typeof delta.$source === "string" ? delta.$source : undefined;
|
|
1222
|
+
// Recording every source keeps each one apart. It gets its own
|
|
1223
|
+
// sampling window: the shared one would keep whichever source arrives
|
|
1224
|
+
// first, so the fastest receiver would crowd out the ones recorded to
|
|
1225
|
+
// compare against it. And its nulls forget only its own last values:
|
|
1226
|
+
// a gateway sending "not available" several times a second would
|
|
1227
|
+
// otherwise make every other source's unchanged values due again at
|
|
1228
|
+
// each sampling window instead of each heartbeat.
|
|
1229
|
+
const perSource = config.recordAllSources ? source : undefined;
|
|
1230
|
+
// Meta updates ride the same bus as values, flagged isMeta. They
|
|
1231
|
+
// describe a path (units, description, displayName), they are not a
|
|
1232
|
+
// reading of it. Routed like a value object, their keys were stored
|
|
1233
|
+
// as rows such as `….units` and `….displayName`, again each time a
|
|
1234
|
+
// plugin republished its meta. Nor do they show that a vessel is
|
|
1235
|
+
// transmitting: a local plugin publishes its meta on its own start,
|
|
1236
|
+
// and counting that would cost the restore a vessel's saved position.
|
|
1237
|
+
if (delta.isMeta)
|
|
1238
|
+
return;
|
|
1215
1239
|
// Note the context BEFORE any early return or filter: identity-only
|
|
1216
1240
|
// deltas return below, and path filters and throttles decide what gets
|
|
1217
|
-
// STORED — but any
|
|
1241
|
+
// STORED — but any value at all proves the vessel is transmitting now,
|
|
1218
1242
|
// which is what makes its stored position obsolete for the restore.
|
|
1219
1243
|
if (trackLiveContexts) {
|
|
1220
1244
|
liveContexts.add(context === app.selfContext ? "self" : context);
|
|
@@ -1257,7 +1281,7 @@ export default (app) => {
|
|
|
1257
1281
|
// the change gate must forget the path, so the first reading after the
|
|
1258
1282
|
// gap is recorded even when it equals the last one before it.
|
|
1259
1283
|
if (path && value === null) {
|
|
1260
|
-
changeGate?.forget(path, context === app.selfContext ? "self" : context);
|
|
1284
|
+
changeGate?.forget(path, context === app.selfContext ? "self" : context, perSource);
|
|
1261
1285
|
}
|
|
1262
1286
|
if (!path || value === undefined || value === null)
|
|
1263
1287
|
return;
|
|
@@ -1304,13 +1328,14 @@ export default (app) => {
|
|
|
1304
1328
|
return;
|
|
1305
1329
|
// Throttled per path AND context: the sampling rate bounds each
|
|
1306
1330
|
// vessel's stream, not the fleet's (issue #93). The stored context
|
|
1307
|
-
// ("self" vs raw) is the key, matching what the rows carry.
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
|
|
1331
|
+
// ("self" vs raw) is the key, matching what the rows carry. Unless
|
|
1332
|
+
// recordAllSources asks for every source, the key deliberately does
|
|
1333
|
+
// NOT include source: the sampling rate bounds the per-path row
|
|
1334
|
+
// volume, and keying per source would multiply it by the number of
|
|
1335
|
+
// receivers. With two sources alternating, each window keeps
|
|
1336
|
+
// whichever arrived first — a source-filtered read then sees roughly
|
|
1337
|
+
// every other sample, which the rate already allows for.
|
|
1338
|
+
if (isThrottled(path, isSelf ? "self" : context, config.defaultSamplingRate ?? 2000, perSource))
|
|
1314
1339
|
return;
|
|
1315
1340
|
}
|
|
1316
1341
|
// Rows are stamped with the server receive time, deliberately NOT the
|
|
@@ -1365,7 +1390,7 @@ export default (app) => {
|
|
|
1365
1390
|
continue;
|
|
1366
1391
|
if (changeGate?.isRepeat(leaf.path, ctx, source, leaf.value, now))
|
|
1367
1392
|
continue;
|
|
1368
|
-
if (isThrottled(leaf.path, ctx, config.defaultSamplingRate ?? 2000))
|
|
1393
|
+
if (isThrottled(leaf.path, ctx, config.defaultSamplingRate ?? 2000, perSource))
|
|
1369
1394
|
continue;
|
|
1370
1395
|
changeGate?.wrote(leaf.path, ctx, source, leaf.value, now);
|
|
1371
1396
|
if (typeof leaf.value === "number") {
|