@sixfathoms/lplex 0.1.0 → 0.2.1

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 CHANGED
@@ -58,6 +58,29 @@ for (const d of devices) {
58
58
  }
59
59
  ```
60
60
 
61
+ ### `client.values(filter?, signal?): Promise<DeviceValues[]>`
62
+
63
+ Returns the last-seen value for each (device, PGN) pair, grouped by device. Useful for getting a snapshot of current bus state without subscribing to SSE.
64
+
65
+ ```typescript
66
+ const snapshot = await client.values();
67
+ for (const device of snapshot) {
68
+ console.log(`${device.manufacturer} (src=${device.src}):`);
69
+ for (const v of device.values) {
70
+ console.log(` PGN ${v.pgn}: ${v.data} @ ${v.ts}`);
71
+ }
72
+ }
73
+ ```
74
+
75
+ Pass a `Filter` to narrow results by PGN and/or device criteria:
76
+
77
+ ```typescript
78
+ const positions = await client.values({
79
+ pgn: [129025],
80
+ manufacturer: ["Garmin"],
81
+ });
82
+ ```
83
+
61
84
  ### `client.subscribe(filter?, signal?): Promise<AsyncIterable<Event>>`
62
85
 
63
86
  Opens an ephemeral SSE stream. No session state, no replay. Frames flow until you stop reading or abort.
@@ -285,6 +308,21 @@ interface SendParams {
285
308
  prio: number;
286
309
  data: string; // hex-encoded
287
310
  }
311
+
312
+ interface PGNValue {
313
+ pgn: number;
314
+ ts: string; // RFC 3339 timestamp
315
+ data: string; // hex-encoded payload
316
+ seq: number; // sequence number
317
+ }
318
+
319
+ interface DeviceValues {
320
+ name: string; // hex CAN NAME (empty if unknown)
321
+ src: number; // source address
322
+ manufacturer?: string;
323
+ model_id?: string;
324
+ values: PGNValue[]; // sorted by PGN
325
+ }
288
326
  ```
289
327
 
290
328
  ## Server Endpoints
@@ -297,6 +335,7 @@ interface SendParams {
297
335
  | `/clients/{id}/ack` | PUT | ACK sequence number. JSON body: `{ "seq": N }`. Returns 204. |
298
336
  | `/send` | POST | Transmit CAN frame. JSON body: `pgn`, `src`, `dst`, `prio`, `data`. Returns 202. |
299
337
  | `/devices` | GET | Device snapshot. Returns JSON array. |
338
+ | `/values` | GET | Last-seen value per (device, PGN). Query params: `pgn`, `manufacturer`, `instance`, `name` (repeatable). Returns JSON array grouped by device. |
300
339
 
301
340
  ## License
302
341
 
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Client: () => Client,
24
+ CloudClient: () => CloudClient,
24
25
  HttpError: () => HttpError,
25
26
  LplexError: () => LplexError,
26
27
  Session: () => Session
@@ -169,6 +170,18 @@ var Client = class {
169
170
  }
170
171
  return resp.json();
171
172
  }
173
+ /** Fetch the last-seen value for each (device, PGN) pair. */
174
+ async values(filter, signal) {
175
+ let url = `${this.#baseURL}/values`;
176
+ const qs = filterToQueryString(filter);
177
+ if (qs) url += `?${qs}`;
178
+ const resp = await this.#fetch(url, { signal });
179
+ if (!resp.ok) {
180
+ const body = await resp.text();
181
+ throw new HttpError("GET", url, resp.status, body);
182
+ }
183
+ return resp.json();
184
+ }
172
185
  /**
173
186
  * Open an ephemeral SSE stream with optional filtering.
174
187
  * No session, no replay, no ACK.
@@ -247,9 +260,64 @@ function filterToJSON(f) {
247
260
  if (f.name?.length) m.name = f.name;
248
261
  return m;
249
262
  }
263
+
264
+ // src/cloud.ts
265
+ var CloudClient = class {
266
+ #baseURL;
267
+ #fetch;
268
+ #fetchOpt;
269
+ constructor(baseURL, options) {
270
+ this.#baseURL = baseURL.replace(/\/+$/, "");
271
+ this.#fetch = options?.fetch ?? globalThis.fetch.bind(globalThis);
272
+ this.#fetchOpt = options ?? {};
273
+ }
274
+ /**
275
+ * Returns a {@link Client} scoped to a specific instance.
276
+ * The returned client's `devices()`, `subscribe()`, etc. hit the
277
+ * cloud's per-instance endpoints.
278
+ */
279
+ client(instanceId) {
280
+ const opts = {};
281
+ if (this.#fetchOpt.fetch) opts.fetch = this.#fetchOpt.fetch;
282
+ return new Client(`${this.#baseURL}/instances/${instanceId}`, opts);
283
+ }
284
+ /** List all known instances. */
285
+ async instances(signal) {
286
+ const url = `${this.#baseURL}/instances`;
287
+ const resp = await this.#fetch(url, { signal });
288
+ if (!resp.ok) {
289
+ const body = await resp.text();
290
+ throw new HttpError("GET", url, resp.status, body);
291
+ }
292
+ const data = await resp.json();
293
+ return data.instances;
294
+ }
295
+ /** Get detailed replication status for one instance. */
296
+ async status(instanceId, signal) {
297
+ const url = `${this.#baseURL}/instances/${instanceId}/status`;
298
+ const resp = await this.#fetch(url, { signal });
299
+ if (!resp.ok) {
300
+ const body = await resp.text();
301
+ throw new HttpError("GET", url, resp.status, body);
302
+ }
303
+ return resp.json();
304
+ }
305
+ /** Fetch recent replication diagnostic events for an instance. */
306
+ async replicationEvents(instanceId, limit, signal) {
307
+ let url = `${this.#baseURL}/instances/${instanceId}/replication/events`;
308
+ if (limit !== void 0) url += `?limit=${limit}`;
309
+ const resp = await this.#fetch(url, { signal });
310
+ if (!resp.ok) {
311
+ const body = await resp.text();
312
+ throw new HttpError("GET", url, resp.status, body);
313
+ }
314
+ return resp.json();
315
+ }
316
+ };
250
317
  // Annotate the CommonJS export names for ESM import in node:
251
318
  0 && (module.exports = {
252
319
  Client,
320
+ CloudClient,
253
321
  HttpError,
254
322
  LplexError,
255
323
  Session
package/dist/index.d.cts CHANGED
@@ -67,12 +67,61 @@ interface SendParams {
67
67
  prio: number;
68
68
  data: string;
69
69
  }
70
+ /** A single PGN's last-known value for a device. */
71
+ interface PGNValue {
72
+ pgn: number;
73
+ ts: string;
74
+ data: string;
75
+ seq: number;
76
+ }
77
+ /** Last-known values grouped by device. */
78
+ interface DeviceValues {
79
+ name: string;
80
+ src: number;
81
+ manufacturer?: string;
82
+ model_id?: string;
83
+ values: PGNValue[];
84
+ }
85
+ /** Summary of a cloud instance, returned by GET /instances. */
86
+ interface InstanceSummary {
87
+ id: string;
88
+ connected: boolean;
89
+ cursor: number;
90
+ boat_head_seq: number;
91
+ holes: number;
92
+ lag_seqs: number;
93
+ last_seen: string;
94
+ }
95
+ /** A sequence range representing a gap in the replication stream. */
96
+ interface SeqRange {
97
+ start: number;
98
+ end: number;
99
+ }
100
+ /** Detailed replication status for one instance. */
101
+ interface InstanceStatus {
102
+ id: string;
103
+ connected: boolean;
104
+ cursor: number;
105
+ boat_head_seq: number;
106
+ boat_journal_bytes: number;
107
+ holes: SeqRange[];
108
+ lag_seqs: number;
109
+ last_seen: string;
110
+ }
111
+ /** Event types emitted by the replication pipeline. */
112
+ type ReplicationEventType = "live_start" | "live_stop" | "backfill_start" | "backfill_stop" | "block_received" | "checkpoint";
113
+ /** A single diagnostic event from the replication pipeline. */
114
+ interface ReplicationEvent {
115
+ time: string;
116
+ type: ReplicationEventType;
117
+ detail?: Record<string, unknown>;
118
+ }
70
119
 
71
- type FetchFn$1 = typeof globalThis.fetch;
120
+ type FetchFn$2 = typeof globalThis.fetch;
72
121
  declare class Session {
73
122
  #private;
74
123
  /** @internal Created by Client.createSession, not for direct use. */
75
- constructor(baseURL: string, fetchFn: FetchFn$1, info: SessionInfo);
124
+ constructor(baseURL: string, fetchFn: FetchFn$2, info: SessionInfo);
76
125
  get info(): SessionInfo;
77
126
  get lastAckedSeq(): number;
78
127
  /**
@@ -84,15 +133,17 @@ declare class Session {
84
133
  ack(seq: number, signal?: AbortSignal): Promise<void>;
85
134
  }
86
135
 
87
- type FetchFn = typeof globalThis.fetch;
136
+ type FetchFn$1 = typeof globalThis.fetch;
88
137
  interface ClientOptions {
89
- fetch?: FetchFn;
138
+ fetch?: FetchFn$1;
90
139
  }
91
140
  declare class Client {
92
141
  #private;
93
142
  constructor(baseURL: string, options?: ClientOptions);
94
143
  /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
95
144
  devices(signal?: AbortSignal): Promise<Device[]>;
145
+ /** Fetch the last-seen value for each (device, PGN) pair. */
146
+ values(filter?: Filter, signal?: AbortSignal): Promise<DeviceValues[]>;
96
147
  /**
97
148
  * Open an ephemeral SSE stream with optional filtering.
98
149
  * No session, no replay, no ACK.
@@ -104,6 +155,33 @@ declare class Client {
104
155
  createSession(config: SessionConfig, signal?: AbortSignal): Promise<Session>;
105
156
  }
106
157
 
158
+ type FetchFn = typeof globalThis.fetch;
159
+ interface CloudClientOptions {
160
+ fetch?: FetchFn;
161
+ }
162
+ /**
163
+ * Client for the lplex-cloud management API.
164
+ *
165
+ * For per-instance data (devices, SSE), use {@link client} to get a
166
+ * regular {@link Client} scoped to that instance.
167
+ */
168
+ declare class CloudClient {
169
+ #private;
170
+ constructor(baseURL: string, options?: CloudClientOptions);
171
+ /**
172
+ * Returns a {@link Client} scoped to a specific instance.
173
+ * The returned client's `devices()`, `subscribe()`, etc. hit the
174
+ * cloud's per-instance endpoints.
175
+ */
176
+ client(instanceId: string): Client;
177
+ /** List all known instances. */
178
+ instances(signal?: AbortSignal): Promise<InstanceSummary[]>;
179
+ /** Get detailed replication status for one instance. */
180
+ status(instanceId: string, signal?: AbortSignal): Promise<InstanceStatus>;
181
+ /** Fetch recent replication diagnostic events for an instance. */
182
+ replicationEvents(instanceId: string, limit?: number, signal?: AbortSignal): Promise<ReplicationEvent[]>;
183
+ }
184
+
107
185
  declare class LplexError extends Error {
108
186
  constructor(message: string);
109
187
  }
@@ -113,4 +191,4 @@ declare class HttpError extends LplexError {
113
191
  constructor(method: string, path: string, status: number, body: string);
114
192
  }
115
193
 
116
- export { Client, type ClientOptions, type Device, type Event, type Filter, type Frame, HttpError, LplexError, type SendParams, Session, type SessionConfig, type SessionInfo };
194
+ export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type Device, type DeviceValues, type Event, type Filter, type Frame, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type ReplicationEvent, type ReplicationEventType, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
package/dist/index.d.ts CHANGED
@@ -67,12 +67,61 @@ interface SendParams {
67
67
  prio: number;
68
68
  data: string;
69
69
  }
70
+ /** A single PGN's last-known value for a device. */
71
+ interface PGNValue {
72
+ pgn: number;
73
+ ts: string;
74
+ data: string;
75
+ seq: number;
76
+ }
77
+ /** Last-known values grouped by device. */
78
+ interface DeviceValues {
79
+ name: string;
80
+ src: number;
81
+ manufacturer?: string;
82
+ model_id?: string;
83
+ values: PGNValue[];
84
+ }
85
+ /** Summary of a cloud instance, returned by GET /instances. */
86
+ interface InstanceSummary {
87
+ id: string;
88
+ connected: boolean;
89
+ cursor: number;
90
+ boat_head_seq: number;
91
+ holes: number;
92
+ lag_seqs: number;
93
+ last_seen: string;
94
+ }
95
+ /** A sequence range representing a gap in the replication stream. */
96
+ interface SeqRange {
97
+ start: number;
98
+ end: number;
99
+ }
100
+ /** Detailed replication status for one instance. */
101
+ interface InstanceStatus {
102
+ id: string;
103
+ connected: boolean;
104
+ cursor: number;
105
+ boat_head_seq: number;
106
+ boat_journal_bytes: number;
107
+ holes: SeqRange[];
108
+ lag_seqs: number;
109
+ last_seen: string;
110
+ }
111
+ /** Event types emitted by the replication pipeline. */
112
+ type ReplicationEventType = "live_start" | "live_stop" | "backfill_start" | "backfill_stop" | "block_received" | "checkpoint";
113
+ /** A single diagnostic event from the replication pipeline. */
114
+ interface ReplicationEvent {
115
+ time: string;
116
+ type: ReplicationEventType;
117
+ detail?: Record<string, unknown>;
118
+ }
70
119
 
71
- type FetchFn$1 = typeof globalThis.fetch;
120
+ type FetchFn$2 = typeof globalThis.fetch;
72
121
  declare class Session {
73
122
  #private;
74
123
  /** @internal Created by Client.createSession, not for direct use. */
75
- constructor(baseURL: string, fetchFn: FetchFn$1, info: SessionInfo);
124
+ constructor(baseURL: string, fetchFn: FetchFn$2, info: SessionInfo);
76
125
  get info(): SessionInfo;
77
126
  get lastAckedSeq(): number;
78
127
  /**
@@ -84,15 +133,17 @@ declare class Session {
84
133
  ack(seq: number, signal?: AbortSignal): Promise<void>;
85
134
  }
86
135
 
87
- type FetchFn = typeof globalThis.fetch;
136
+ type FetchFn$1 = typeof globalThis.fetch;
88
137
  interface ClientOptions {
89
- fetch?: FetchFn;
138
+ fetch?: FetchFn$1;
90
139
  }
91
140
  declare class Client {
92
141
  #private;
93
142
  constructor(baseURL: string, options?: ClientOptions);
94
143
  /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
95
144
  devices(signal?: AbortSignal): Promise<Device[]>;
145
+ /** Fetch the last-seen value for each (device, PGN) pair. */
146
+ values(filter?: Filter, signal?: AbortSignal): Promise<DeviceValues[]>;
96
147
  /**
97
148
  * Open an ephemeral SSE stream with optional filtering.
98
149
  * No session, no replay, no ACK.
@@ -104,6 +155,33 @@ declare class Client {
104
155
  createSession(config: SessionConfig, signal?: AbortSignal): Promise<Session>;
105
156
  }
106
157
 
158
+ type FetchFn = typeof globalThis.fetch;
159
+ interface CloudClientOptions {
160
+ fetch?: FetchFn;
161
+ }
162
+ /**
163
+ * Client for the lplex-cloud management API.
164
+ *
165
+ * For per-instance data (devices, SSE), use {@link client} to get a
166
+ * regular {@link Client} scoped to that instance.
167
+ */
168
+ declare class CloudClient {
169
+ #private;
170
+ constructor(baseURL: string, options?: CloudClientOptions);
171
+ /**
172
+ * Returns a {@link Client} scoped to a specific instance.
173
+ * The returned client's `devices()`, `subscribe()`, etc. hit the
174
+ * cloud's per-instance endpoints.
175
+ */
176
+ client(instanceId: string): Client;
177
+ /** List all known instances. */
178
+ instances(signal?: AbortSignal): Promise<InstanceSummary[]>;
179
+ /** Get detailed replication status for one instance. */
180
+ status(instanceId: string, signal?: AbortSignal): Promise<InstanceStatus>;
181
+ /** Fetch recent replication diagnostic events for an instance. */
182
+ replicationEvents(instanceId: string, limit?: number, signal?: AbortSignal): Promise<ReplicationEvent[]>;
183
+ }
184
+
107
185
  declare class LplexError extends Error {
108
186
  constructor(message: string);
109
187
  }
@@ -113,4 +191,4 @@ declare class HttpError extends LplexError {
113
191
  constructor(method: string, path: string, status: number, body: string);
114
192
  }
115
193
 
116
- export { Client, type ClientOptions, type Device, type Event, type Filter, type Frame, HttpError, LplexError, type SendParams, Session, type SessionConfig, type SessionInfo };
194
+ export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type Device, type DeviceValues, type Event, type Filter, type Frame, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type ReplicationEvent, type ReplicationEventType, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
package/dist/index.js CHANGED
@@ -140,6 +140,18 @@ var Client = class {
140
140
  }
141
141
  return resp.json();
142
142
  }
143
+ /** Fetch the last-seen value for each (device, PGN) pair. */
144
+ async values(filter, signal) {
145
+ let url = `${this.#baseURL}/values`;
146
+ const qs = filterToQueryString(filter);
147
+ if (qs) url += `?${qs}`;
148
+ const resp = await this.#fetch(url, { signal });
149
+ if (!resp.ok) {
150
+ const body = await resp.text();
151
+ throw new HttpError("GET", url, resp.status, body);
152
+ }
153
+ return resp.json();
154
+ }
143
155
  /**
144
156
  * Open an ephemeral SSE stream with optional filtering.
145
157
  * No session, no replay, no ACK.
@@ -218,8 +230,63 @@ function filterToJSON(f) {
218
230
  if (f.name?.length) m.name = f.name;
219
231
  return m;
220
232
  }
233
+
234
+ // src/cloud.ts
235
+ var CloudClient = class {
236
+ #baseURL;
237
+ #fetch;
238
+ #fetchOpt;
239
+ constructor(baseURL, options) {
240
+ this.#baseURL = baseURL.replace(/\/+$/, "");
241
+ this.#fetch = options?.fetch ?? globalThis.fetch.bind(globalThis);
242
+ this.#fetchOpt = options ?? {};
243
+ }
244
+ /**
245
+ * Returns a {@link Client} scoped to a specific instance.
246
+ * The returned client's `devices()`, `subscribe()`, etc. hit the
247
+ * cloud's per-instance endpoints.
248
+ */
249
+ client(instanceId) {
250
+ const opts = {};
251
+ if (this.#fetchOpt.fetch) opts.fetch = this.#fetchOpt.fetch;
252
+ return new Client(`${this.#baseURL}/instances/${instanceId}`, opts);
253
+ }
254
+ /** List all known instances. */
255
+ async instances(signal) {
256
+ const url = `${this.#baseURL}/instances`;
257
+ const resp = await this.#fetch(url, { signal });
258
+ if (!resp.ok) {
259
+ const body = await resp.text();
260
+ throw new HttpError("GET", url, resp.status, body);
261
+ }
262
+ const data = await resp.json();
263
+ return data.instances;
264
+ }
265
+ /** Get detailed replication status for one instance. */
266
+ async status(instanceId, signal) {
267
+ const url = `${this.#baseURL}/instances/${instanceId}/status`;
268
+ const resp = await this.#fetch(url, { signal });
269
+ if (!resp.ok) {
270
+ const body = await resp.text();
271
+ throw new HttpError("GET", url, resp.status, body);
272
+ }
273
+ return resp.json();
274
+ }
275
+ /** Fetch recent replication diagnostic events for an instance. */
276
+ async replicationEvents(instanceId, limit, signal) {
277
+ let url = `${this.#baseURL}/instances/${instanceId}/replication/events`;
278
+ if (limit !== void 0) url += `?limit=${limit}`;
279
+ const resp = await this.#fetch(url, { signal });
280
+ if (!resp.ok) {
281
+ const body = await resp.text();
282
+ throw new HttpError("GET", url, resp.status, body);
283
+ }
284
+ return resp.json();
285
+ }
286
+ };
221
287
  export {
222
288
  Client,
289
+ CloudClient,
223
290
  HttpError,
224
291
  LplexError,
225
292
  Session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixfathoms/lplex",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "TypeScript client for lplex CAN bus HTTP bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",