signalk-siparu 0.1.21 → 0.1.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-siparu",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Kept aboard, proven ashore. An impartial, timestamped record of every voyage: position, wind, depth and logbook, written on the boat and readable from anywhere. A read-only Signal K plugin with a built-in dashboard.",
5
5
  "keywords": [
6
6
  "signalk-node-server-plugin",
@@ -189,6 +189,32 @@ export type SnapshotsResponse = {
189
189
  message: string;
190
190
  };
191
191
  };
192
+ /**
193
+ * The voyages RPC, a third sibling on the same live socket. Where history asks for one gauge's
194
+ * series and snapshots for whole rows over a window, this asks for the boat's recent voyages -
195
+ * the list the local /voyages REST serves. It carries no query, only how many of the newest to
196
+ * return; like its siblings it is a read of the boat's own store and reaches nothing near
197
+ * Signal K. The boat clamps the count, so a request cannot ask for more than she will give.
198
+ */
199
+ export interface VoyagesRequest {
200
+ type: 'voyages';
201
+ id: string;
202
+ /** How many of the newest voyages to return. Clamped boat-side to the REST bounds. */
203
+ limit: number;
204
+ }
205
+ /** The boat's answer to one VoyagesRequest. The voyages or a reason, never both. */
206
+ export type VoyagesResponse = {
207
+ type: 'voyages';
208
+ id: string;
209
+ result: VoyageListResult;
210
+ } | {
211
+ type: 'voyages';
212
+ id: string;
213
+ error: {
214
+ code: string;
215
+ message: string;
216
+ };
217
+ };
192
218
  export interface LiveResult extends Snapshot {
193
219
  /** Seconds since the newest delta touched any subscribed path; null before first delta. */
194
220
  data_age_s: number | null;
@@ -295,6 +321,10 @@ export interface Voyage {
295
321
  end_port: string | null;
296
322
  status: 'open' | 'closed';
297
323
  }
324
+ /** The boat's answer to one VoyagesRequest: her recent voyages, newest first. */
325
+ export interface VoyageListResult {
326
+ voyages: Voyage[];
327
+ }
298
328
  /** One aggregation window on the voyage stats card. */
299
329
  export interface VoyageWindowStats {
300
330
  distance_nm: number;
@@ -349,6 +349,11 @@ module.exports = (app) => {
349
349
  // Her whole recorded rows over a window, the logbook read - the same store the local
350
350
  // /snapshots serves, reached here and nowhere near Signal K.
351
351
  onSnapshotsQuery: (q) => qs.snapshots(q, Date.now()),
352
+ // Her recent voyages, the list the local /voyages serves. The count is clamped to the
353
+ // same 1..500 bounds the REST route enforces, since vl.list does not clamp its own.
354
+ onVoyagesQuery: async (limit) => ({
355
+ voyages: vl.list(Math.min(Math.max(1, limit || 50), 500))
356
+ }),
352
357
  debug: (msg) => app.debug(msg)
353
358
  });
354
359
  liveUplink = ws;
@@ -16,7 +16,7 @@
16
16
  * fleet, and every owner would have to walk down to their boat to fix a bug that was ours.
17
17
  */
18
18
  import type { RemoteLink } from './remotelink';
19
- import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult } from './contract';
19
+ import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult, VoyageListResult } from './contract';
20
20
  /**
21
21
  * How often a frame goes up while the socket is open, when she is under way.
22
22
  *
@@ -99,6 +99,13 @@ export interface LiveDeps {
99
99
  * so an old relay or a boat wired without it simply never grows the ear.
100
100
  */
101
101
  onSnapshotsQuery?: (query: SnapshotsQuery) => Promise<SnapshotsResult>;
102
+ /**
103
+ * Answers a shore voyages request - her recent voyages, the list the local REST /voyages
104
+ * serves - from the same store. A third sibling of onHistoryQuery: a read, never a command,
105
+ * and it never reaches Signal K. Absent leaves the socket deaf to voyages requests, so an old
106
+ * relay or a boat wired without it simply never grows the ear.
107
+ */
108
+ onVoyagesQuery?: (limit: number) => Promise<VoyageListResult>;
102
109
  debug: (msg: string) => void;
103
110
  /** Injected in tests. In production this is the `ws` adapter at the bottom of the file. */
104
111
  connect?: (url: string, token: string) => LiveSocket;
@@ -186,9 +193,17 @@ export declare class LiveUplink {
186
193
  */
187
194
  private handleSnapshots;
188
195
  /**
189
- * Send a history or snapshots answer, but only if it still belongs to the socket that asked.
190
- * A query reads the disk while the line may drop and redial underneath it; the generation
191
- * guard is what keeps a slow answer from landing on a fresh connection that never asked.
196
+ * A voyages request from the shore, answered from the boat's own store - a third sibling of
197
+ * handleHistory, and just as narrow. Parse, act only if it is a voyages request, and read the
198
+ * store, never Signal K. The boat clamps the count before it reads, so a request cannot ask
199
+ * her for more than she will give.
200
+ */
201
+ private handleVoyages;
202
+ /**
203
+ * Send a history, snapshots or voyages answer, but only if it still belongs to the socket that
204
+ * asked. A query reads the disk while the line may drop and redial underneath it; the
205
+ * generation guard is what keeps a slow answer from landing on a fresh connection that never
206
+ * asked.
192
207
  */
193
208
  private reply;
194
209
  private keepalive;
@@ -202,11 +202,13 @@ class LiveUplink {
202
202
  return;
203
203
  }
204
204
  // Beyond a pong, the shore may ask the boat to send back her own recorded history: one
205
- // gauge's series (handleHistory) or whole snapshot rows (handleSnapshots). Neither is a
206
- // command; each drops in silence anything that is not its own request, and anything that
207
- // is neither is not acted on at all, because the shore may not steer a boat.
205
+ // gauge's series (handleHistory), whole snapshot rows (handleSnapshots) or her recent
206
+ // voyages (handleVoyages). None is a command; each drops in silence anything that is not
207
+ // its own request, and anything that is none is not acted on at all, because the shore
208
+ // may not steer a boat.
208
209
  this.handleHistory(gen, data);
209
210
  this.handleSnapshots(gen, data);
211
+ this.handleVoyages(gen, data);
210
212
  });
211
213
  sock.onClose((code) => {
212
214
  if (gen !== this.gen)
@@ -353,9 +355,39 @@ class LiveUplink {
353
355
  });
354
356
  }
355
357
  /**
356
- * Send a history or snapshots answer, but only if it still belongs to the socket that asked.
357
- * A query reads the disk while the line may drop and redial underneath it; the generation
358
- * guard is what keeps a slow answer from landing on a fresh connection that never asked.
358
+ * A voyages request from the shore, answered from the boat's own store - a third sibling of
359
+ * handleHistory, and just as narrow. Parse, act only if it is a voyages request, and read the
360
+ * store, never Signal K. The boat clamps the count before it reads, so a request cannot ask
361
+ * her for more than she will give.
362
+ */
363
+ handleVoyages(gen, data) {
364
+ const handler = this.deps.onVoyagesQuery;
365
+ if (!handler)
366
+ return;
367
+ let msg;
368
+ try {
369
+ msg = JSON.parse(data);
370
+ }
371
+ catch {
372
+ return;
373
+ }
374
+ if (!isVoyagesRequest(msg))
375
+ return;
376
+ const { id, limit } = msg;
377
+ handler(limit).then((result) => this.reply(gen, { type: 'voyages', id, result }), (err) => {
378
+ this.deps.debug(`voyages query failed: ${String(err)}`);
379
+ this.reply(gen, {
380
+ type: 'voyages',
381
+ id,
382
+ error: { code: 'VOYAGES_FAILED', message: 'voyages query failed' }
383
+ });
384
+ });
385
+ }
386
+ /**
387
+ * Send a history, snapshots or voyages answer, but only if it still belongs to the socket that
388
+ * asked. A query reads the disk while the line may drop and redial underneath it; the
389
+ * generation guard is what keeps a slow answer from landing on a fresh connection that never
390
+ * asked.
359
391
  */
360
392
  reply(gen, msg) {
361
393
  if (gen !== this.gen || !this.sock)
@@ -493,6 +525,17 @@ function isSnapshotsRequest(m) {
493
525
  typeof o.query === 'object' &&
494
526
  o.query !== null);
495
527
  }
528
+ /**
529
+ * A voyages request, told apart the same way: the type tag is the gate. It carries no query,
530
+ * only a count - so the tag, the id and a numeric limit are checked. The limit's bounds are the
531
+ * boat's to enforce (she clamps it before reading), so they are not re-checked here.
532
+ */
533
+ function isVoyagesRequest(m) {
534
+ if (typeof m !== 'object' || m === null)
535
+ return false;
536
+ const o = m;
537
+ return o.type === 'voyages' && typeof o.id === 'string' && typeof o.limit === 'number';
538
+ }
496
539
  /**
497
540
  * The real socket.
498
541
  *