@sixfathoms/lplex 0.3.0 → 0.4.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/dist/index.cjs CHANGED
@@ -89,8 +89,16 @@ async function* parseSSE(body) {
89
89
  }
90
90
  }
91
91
  function classify(obj) {
92
- if ("type" in obj && obj.type === "device") {
93
- return { type: "device", device: obj };
92
+ if ("type" in obj) {
93
+ if (obj.type === "device") {
94
+ return { type: "device", device: obj };
95
+ }
96
+ if (obj.type === "device_removed") {
97
+ return {
98
+ type: "device_removed",
99
+ deviceRemoved: obj
100
+ };
101
+ }
94
102
  }
95
103
  if ("seq" in obj) {
96
104
  return { type: "frame", frame: obj };
@@ -185,14 +193,27 @@ var Client = class {
185
193
  /**
186
194
  * Open an ephemeral SSE stream with optional filtering.
187
195
  * No session, no replay, no ACK.
196
+ *
197
+ * Accepts either a Filter (for backwards compatibility) or
198
+ * SubscribeOptions for additional control (decode, signal).
188
199
  */
189
- async subscribe(filter, signal) {
200
+ async subscribe(filterOrOptions, signal) {
201
+ let filter;
202
+ let decode;
203
+ let sig = signal;
204
+ if (filterOrOptions && isSubscribeOptions(filterOrOptions)) {
205
+ filter = filterOrOptions.filter;
206
+ decode = filterOrOptions.decode;
207
+ sig = filterOrOptions.signal ?? sig;
208
+ } else {
209
+ filter = filterOrOptions;
210
+ }
190
211
  let url = `${this.#baseURL}/events`;
191
- const qs = filterToQueryString(filter);
212
+ const qs = filterToQueryString(filter, decode);
192
213
  if (qs) url += `?${qs}`;
193
214
  const resp = await this.#fetch(url, {
194
215
  headers: { Accept: "text/event-stream" },
195
- signal
216
+ signal: sig
196
217
  });
197
218
  if (!resp.ok) {
198
219
  const body = await resp.text();
@@ -231,7 +252,7 @@ var Client = class {
231
252
  }
232
253
  /**
233
254
  * Send an ISO Request (PGN 59904) and wait for the response frame.
234
- * Returns the response frame, or throws HttpError with status 504 on timeout.
255
+ * Returns the response frame, or throws HttpError on timeout (408).
235
256
  */
236
257
  async query(params, signal) {
237
258
  const url = `${this.#baseURL}/query`;
@@ -247,7 +268,28 @@ var Client = class {
247
268
  }
248
269
  return resp.json();
249
270
  }
250
- /** Check server health. */
271
+ /**
272
+ * Query historical frames (requires journaling on the server).
273
+ * Returns an array of frames matching the query parameters.
274
+ */
275
+ async history(params, signal) {
276
+ const qs = new URLSearchParams();
277
+ qs.set("from", params.from);
278
+ if (params.to) qs.set("to", params.to);
279
+ for (const p of params.pgn ?? []) qs.append("pgn", p.toString());
280
+ for (const s of params.src ?? []) qs.append("src", s.toString());
281
+ if (params.limit !== void 0) qs.set("limit", params.limit.toString());
282
+ if (params.interval) qs.set("interval", params.interval);
283
+ if (params.decode) qs.set("decode", "true");
284
+ const url = `${this.#baseURL}/history?${qs.toString()}`;
285
+ const resp = await this.#fetch(url, { signal });
286
+ if (!resp.ok) {
287
+ const body = await resp.text();
288
+ throw new HttpError("GET", url, resp.status, body);
289
+ }
290
+ return resp.json();
291
+ }
292
+ /** Check server health (GET /healthz). */
251
293
  async health(signal) {
252
294
  const url = `${this.#baseURL}/healthz`;
253
295
  const resp = await this.#fetch(url, { signal });
@@ -257,6 +299,26 @@ var Client = class {
257
299
  }
258
300
  return resp.json();
259
301
  }
302
+ /** Liveness probe (GET /livez). */
303
+ async liveness(signal) {
304
+ const url = `${this.#baseURL}/livez`;
305
+ const resp = await this.#fetch(url, { signal });
306
+ if (!resp.ok) {
307
+ const body = await resp.text();
308
+ throw new HttpError("GET", url, resp.status, body);
309
+ }
310
+ return resp.json();
311
+ }
312
+ /** Readiness probe (GET /readyz). */
313
+ async readiness(signal) {
314
+ const url = `${this.#baseURL}/readyz`;
315
+ const resp = await this.#fetch(url, { signal });
316
+ if (!resp.ok) {
317
+ const body = await resp.text();
318
+ throw new HttpError("GET", url, resp.status, body);
319
+ }
320
+ return resp.json();
321
+ }
260
322
  /** Fetch boat-side replication status (only available when replication is configured). */
261
323
  async replicationStatus(signal) {
262
324
  const url = `${this.#baseURL}/replication/status`;
@@ -290,19 +352,26 @@ var Client = class {
290
352
  return new Session(this.#baseURL, this.#fetch, info);
291
353
  }
292
354
  };
355
+ function isSubscribeOptions(obj) {
356
+ return "decode" in obj || "signal" in obj || "filter" in obj;
357
+ }
293
358
  function filterIsEmpty(f) {
294
- return !f.pgn?.length && !f.exclude_pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length && !f.exclude_name?.length;
359
+ return !f.pgn?.length && !f.exclude_pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length && !f.exclude_name?.length && !f.bus?.length;
295
360
  }
296
- function filterToQueryString(f) {
297
- if (!f || filterIsEmpty(f)) return "";
361
+ function filterToQueryString(f, decode) {
362
+ if ((!f || filterIsEmpty(f)) && !decode) return "";
298
363
  const params = new URLSearchParams();
299
- for (const p of f.pgn ?? []) params.append("pgn", p.toString());
300
- for (const p of f.exclude_pgn ?? [])
301
- params.append("exclude_pgn", p.toString());
302
- for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
303
- for (const i of f.instance ?? []) params.append("instance", i.toString());
304
- for (const n of f.name ?? []) params.append("name", n);
305
- for (const n of f.exclude_name ?? []) params.append("exclude_name", n);
364
+ if (f) {
365
+ for (const p of f.pgn ?? []) params.append("pgn", p.toString());
366
+ for (const p of f.exclude_pgn ?? [])
367
+ params.append("exclude_pgn", p.toString());
368
+ for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
369
+ for (const i of f.instance ?? []) params.append("instance", i.toString());
370
+ for (const n of f.name ?? []) params.append("name", n);
371
+ for (const n of f.exclude_name ?? []) params.append("exclude_name", n);
372
+ for (const b of f.bus ?? []) params.append("bus", b);
373
+ }
374
+ if (decode) params.set("decode", "true");
306
375
  return params.toString();
307
376
  }
308
377
  function filterToJSON(f) {
@@ -313,6 +382,7 @@ function filterToJSON(f) {
313
382
  if (f.instance?.length) m.instance = f.instance;
314
383
  if (f.name?.length) m.name = f.name;
315
384
  if (f.exclude_name?.length) m.exclude_name = f.exclude_name;
385
+ if (f.bus?.length) m.bus = f.bus;
316
386
  return m;
317
387
  }
318
388
 
@@ -368,6 +438,36 @@ var CloudClient = class {
368
438
  }
369
439
  return resp.json();
370
440
  }
441
+ /** Check cloud server health (GET /healthz). */
442
+ async health(signal) {
443
+ const url = `${this.#baseURL}/healthz`;
444
+ const resp = await this.#fetch(url, { signal });
445
+ if (!resp.ok) {
446
+ const body = await resp.text();
447
+ throw new HttpError("GET", url, resp.status, body);
448
+ }
449
+ return resp.json();
450
+ }
451
+ /** Liveness probe (GET /livez). */
452
+ async liveness(signal) {
453
+ const url = `${this.#baseURL}/livez`;
454
+ const resp = await this.#fetch(url, { signal });
455
+ if (!resp.ok) {
456
+ const body = await resp.text();
457
+ throw new HttpError("GET", url, resp.status, body);
458
+ }
459
+ return resp.json();
460
+ }
461
+ /** Readiness probe (GET /readyz). */
462
+ async readiness(signal) {
463
+ const url = `${this.#baseURL}/readyz`;
464
+ const resp = await this.#fetch(url, { signal });
465
+ if (!resp.ok) {
466
+ const body = await resp.text();
467
+ throw new HttpError("GET", url, resp.status, body);
468
+ }
469
+ return resp.json();
470
+ }
371
471
  };
372
472
  // Annotate the CommonJS export names for ESM import in node:
373
473
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -2,14 +2,17 @@
2
2
  interface Frame {
3
3
  seq: number;
4
4
  ts: string;
5
+ bus?: string;
5
6
  prio: number;
6
7
  pgn: number;
7
8
  src: number;
8
9
  dst: number;
9
10
  data: string;
11
+ decoded?: Record<string, unknown>;
10
12
  }
11
13
  /** An NMEA 2000 device discovered on the bus. */
12
14
  interface Device {
15
+ bus?: string;
13
16
  src: number;
14
17
  name: string;
15
18
  manufacturer: string;
@@ -28,6 +31,12 @@ interface Device {
28
31
  packet_count: number;
29
32
  byte_count: number;
30
33
  }
34
+ /** A device-removed notification from the bus. */
35
+ interface DeviceRemoved {
36
+ type: "device_removed";
37
+ bus?: string;
38
+ src: number;
39
+ }
31
40
  /** Discriminated union for SSE events. */
32
41
  type Event = {
33
42
  type: "frame";
@@ -35,6 +44,9 @@ type Event = {
35
44
  } | {
36
45
  type: "device";
37
46
  device: Device;
47
+ } | {
48
+ type: "device_removed";
49
+ deviceRemoved: DeviceRemoved;
38
50
  };
39
51
  /**
40
52
  * Filter for CAN frames.
@@ -47,6 +59,14 @@ interface Filter {
47
59
  instance?: number[];
48
60
  name?: string[];
49
61
  exclude_name?: string[];
62
+ bus?: string[];
63
+ }
64
+ /** Options for ephemeral SSE subscription. */
65
+ interface SubscribeOptions {
66
+ filter?: Filter;
67
+ /** When true, frames include decoded field values. */
68
+ decode?: boolean;
69
+ signal?: AbortSignal;
50
70
  }
51
71
  /** Configuration for creating a buffered session. */
52
72
  interface SessionConfig {
@@ -68,6 +88,7 @@ interface SendParams {
68
88
  dst: number;
69
89
  prio: number;
70
90
  data: string;
91
+ bus?: string;
71
92
  }
72
93
  /** A single PGN's last-known value for a device. */
73
94
  interface PGNValue {
@@ -103,12 +124,56 @@ interface DecodedDeviceValues {
103
124
  /** Parameters for an ISO Request query (POST /query). */
104
125
  interface QueryParams {
105
126
  pgn: number;
106
- dst: number;
127
+ /** Destination address. Defaults to 0xFF (broadcast) on the server. */
128
+ dst?: number;
107
129
  timeout?: string;
130
+ bus?: string;
131
+ }
132
+ /** Parameters for historical data query (GET /history). */
133
+ interface HistoryParams {
134
+ /** Start timestamp (RFC 3339). */
135
+ from: string;
136
+ /** End timestamp (RFC 3339). Defaults to now. */
137
+ to?: string;
138
+ /** Filter by PGN(s). */
139
+ pgn?: number[];
140
+ /** Filter by source address(es). */
141
+ src?: number[];
142
+ /** Max frames to return. Defaults to 10000. */
143
+ limit?: number;
144
+ /** Downsample interval (e.g. "1s", "PT1M"). */
145
+ interval?: string;
146
+ /** Include decoded values in response. */
147
+ decode?: boolean;
148
+ }
149
+ /** Broker health details. */
150
+ interface BrokerHealth {
151
+ status: string;
152
+ frames_total: number;
153
+ head_seq: number;
154
+ last_frame_time: string;
155
+ device_count: number;
156
+ ring_entries: number;
157
+ ring_capacity: number;
108
158
  }
109
- /** Health check response from GET /healthz. */
159
+ /** Replication component health (within health response). */
160
+ interface ReplicationHealth {
161
+ status: string;
162
+ connected: boolean;
163
+ live_lag: number;
164
+ backfill_remaining_seqs: number;
165
+ last_ack: string;
166
+ }
167
+ /** Health check response from GET /healthz or /readyz. */
110
168
  interface HealthStatus {
111
169
  status: string;
170
+ broker?: BrokerHealth;
171
+ replication?: ReplicationHealth;
172
+ components?: Record<string, unknown>;
173
+ /** Cloud-only: total known instances. */
174
+ instances_total?: number;
175
+ /** Cloud-only: currently connected instances. */
176
+ instances_connected?: number;
112
177
  }
113
178
  /** Boat-side replication status from GET /replication/status. */
114
179
  interface ReplicationStatus {
@@ -120,6 +185,10 @@ interface ReplicationStatus {
120
185
  live_lag: number;
121
186
  backfill_remaining_seqs: number;
122
187
  last_ack: string;
188
+ live_frames_sent: number;
189
+ backfill_blocks_sent: number;
190
+ backfill_bytes_sent: number;
191
+ reconnects: number;
123
192
  }
124
193
  /** Summary of a cloud instance, returned by GET /instances. */
125
194
  interface InstanceSummary {
@@ -186,19 +255,31 @@ declare class Client {
186
255
  /**
187
256
  * Open an ephemeral SSE stream with optional filtering.
188
257
  * No session, no replay, no ACK.
258
+ *
259
+ * Accepts either a Filter (for backwards compatibility) or
260
+ * SubscribeOptions for additional control (decode, signal).
189
261
  */
190
- subscribe(filter?: Filter, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
262
+ subscribe(filterOrOptions?: Filter | SubscribeOptions, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
191
263
  /** Fetch the last-seen decoded values for each (device, PGN) pair. */
192
264
  decodedValues(filter?: Filter, signal?: AbortSignal): Promise<DecodedDeviceValues[]>;
193
265
  /** Transmit a CAN frame through the server. */
194
266
  send(params: SendParams, signal?: AbortSignal): Promise<void>;
195
267
  /**
196
268
  * Send an ISO Request (PGN 59904) and wait for the response frame.
197
- * Returns the response frame, or throws HttpError with status 504 on timeout.
269
+ * Returns the response frame, or throws HttpError on timeout (408).
198
270
  */
199
271
  query(params: QueryParams, signal?: AbortSignal): Promise<Frame>;
200
- /** Check server health. */
272
+ /**
273
+ * Query historical frames (requires journaling on the server).
274
+ * Returns an array of frames matching the query parameters.
275
+ */
276
+ history(params: HistoryParams, signal?: AbortSignal): Promise<Frame[]>;
277
+ /** Check server health (GET /healthz). */
201
278
  health(signal?: AbortSignal): Promise<HealthStatus>;
279
+ /** Liveness probe (GET /livez). */
280
+ liveness(signal?: AbortSignal): Promise<HealthStatus>;
281
+ /** Readiness probe (GET /readyz). */
282
+ readiness(signal?: AbortSignal): Promise<HealthStatus>;
202
283
  /** Fetch boat-side replication status (only available when replication is configured). */
203
284
  replicationStatus(signal?: AbortSignal): Promise<ReplicationStatus>;
204
285
  /** Create or reconnect a buffered session on the server. */
@@ -230,6 +311,12 @@ declare class CloudClient {
230
311
  status(instanceId: string, signal?: AbortSignal): Promise<InstanceStatus>;
231
312
  /** Fetch recent replication diagnostic events for an instance. */
232
313
  replicationEvents(instanceId: string, limit?: number, signal?: AbortSignal): Promise<ReplicationEvent[]>;
314
+ /** Check cloud server health (GET /healthz). */
315
+ health(signal?: AbortSignal): Promise<HealthStatus>;
316
+ /** Liveness probe (GET /livez). */
317
+ liveness(signal?: AbortSignal): Promise<HealthStatus>;
318
+ /** Readiness probe (GET /readyz). */
319
+ readiness(signal?: AbortSignal): Promise<HealthStatus>;
233
320
  }
234
321
 
235
322
  declare class LplexError extends Error {
@@ -241,4 +328,4 @@ declare class HttpError extends LplexError {
241
328
  constructor(method: string, path: string, status: number, body: string);
242
329
  }
243
330
 
244
- export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type DecodedDeviceValues, type DecodedPGNValue, type Device, type DeviceValues, type Event, type Filter, type Frame, type HealthStatus, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type QueryParams, type ReplicationEvent, type ReplicationEventType, type ReplicationStatus, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
331
+ export { type BrokerHealth, Client, type ClientOptions, CloudClient, type CloudClientOptions, type DecodedDeviceValues, type DecodedPGNValue, type Device, type DeviceRemoved, type DeviceValues, type Event, type Filter, type Frame, type HealthStatus, type HistoryParams, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type QueryParams, type ReplicationEvent, type ReplicationEventType, type ReplicationHealth, type ReplicationStatus, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo, type SubscribeOptions };
package/dist/index.d.ts CHANGED
@@ -2,14 +2,17 @@
2
2
  interface Frame {
3
3
  seq: number;
4
4
  ts: string;
5
+ bus?: string;
5
6
  prio: number;
6
7
  pgn: number;
7
8
  src: number;
8
9
  dst: number;
9
10
  data: string;
11
+ decoded?: Record<string, unknown>;
10
12
  }
11
13
  /** An NMEA 2000 device discovered on the bus. */
12
14
  interface Device {
15
+ bus?: string;
13
16
  src: number;
14
17
  name: string;
15
18
  manufacturer: string;
@@ -28,6 +31,12 @@ interface Device {
28
31
  packet_count: number;
29
32
  byte_count: number;
30
33
  }
34
+ /** A device-removed notification from the bus. */
35
+ interface DeviceRemoved {
36
+ type: "device_removed";
37
+ bus?: string;
38
+ src: number;
39
+ }
31
40
  /** Discriminated union for SSE events. */
32
41
  type Event = {
33
42
  type: "frame";
@@ -35,6 +44,9 @@ type Event = {
35
44
  } | {
36
45
  type: "device";
37
46
  device: Device;
47
+ } | {
48
+ type: "device_removed";
49
+ deviceRemoved: DeviceRemoved;
38
50
  };
39
51
  /**
40
52
  * Filter for CAN frames.
@@ -47,6 +59,14 @@ interface Filter {
47
59
  instance?: number[];
48
60
  name?: string[];
49
61
  exclude_name?: string[];
62
+ bus?: string[];
63
+ }
64
+ /** Options for ephemeral SSE subscription. */
65
+ interface SubscribeOptions {
66
+ filter?: Filter;
67
+ /** When true, frames include decoded field values. */
68
+ decode?: boolean;
69
+ signal?: AbortSignal;
50
70
  }
51
71
  /** Configuration for creating a buffered session. */
52
72
  interface SessionConfig {
@@ -68,6 +88,7 @@ interface SendParams {
68
88
  dst: number;
69
89
  prio: number;
70
90
  data: string;
91
+ bus?: string;
71
92
  }
72
93
  /** A single PGN's last-known value for a device. */
73
94
  interface PGNValue {
@@ -103,12 +124,56 @@ interface DecodedDeviceValues {
103
124
  /** Parameters for an ISO Request query (POST /query). */
104
125
  interface QueryParams {
105
126
  pgn: number;
106
- dst: number;
127
+ /** Destination address. Defaults to 0xFF (broadcast) on the server. */
128
+ dst?: number;
107
129
  timeout?: string;
130
+ bus?: string;
131
+ }
132
+ /** Parameters for historical data query (GET /history). */
133
+ interface HistoryParams {
134
+ /** Start timestamp (RFC 3339). */
135
+ from: string;
136
+ /** End timestamp (RFC 3339). Defaults to now. */
137
+ to?: string;
138
+ /** Filter by PGN(s). */
139
+ pgn?: number[];
140
+ /** Filter by source address(es). */
141
+ src?: number[];
142
+ /** Max frames to return. Defaults to 10000. */
143
+ limit?: number;
144
+ /** Downsample interval (e.g. "1s", "PT1M"). */
145
+ interval?: string;
146
+ /** Include decoded values in response. */
147
+ decode?: boolean;
148
+ }
149
+ /** Broker health details. */
150
+ interface BrokerHealth {
151
+ status: string;
152
+ frames_total: number;
153
+ head_seq: number;
154
+ last_frame_time: string;
155
+ device_count: number;
156
+ ring_entries: number;
157
+ ring_capacity: number;
108
158
  }
109
- /** Health check response from GET /healthz. */
159
+ /** Replication component health (within health response). */
160
+ interface ReplicationHealth {
161
+ status: string;
162
+ connected: boolean;
163
+ live_lag: number;
164
+ backfill_remaining_seqs: number;
165
+ last_ack: string;
166
+ }
167
+ /** Health check response from GET /healthz or /readyz. */
110
168
  interface HealthStatus {
111
169
  status: string;
170
+ broker?: BrokerHealth;
171
+ replication?: ReplicationHealth;
172
+ components?: Record<string, unknown>;
173
+ /** Cloud-only: total known instances. */
174
+ instances_total?: number;
175
+ /** Cloud-only: currently connected instances. */
176
+ instances_connected?: number;
112
177
  }
113
178
  /** Boat-side replication status from GET /replication/status. */
114
179
  interface ReplicationStatus {
@@ -120,6 +185,10 @@ interface ReplicationStatus {
120
185
  live_lag: number;
121
186
  backfill_remaining_seqs: number;
122
187
  last_ack: string;
188
+ live_frames_sent: number;
189
+ backfill_blocks_sent: number;
190
+ backfill_bytes_sent: number;
191
+ reconnects: number;
123
192
  }
124
193
  /** Summary of a cloud instance, returned by GET /instances. */
125
194
  interface InstanceSummary {
@@ -186,19 +255,31 @@ declare class Client {
186
255
  /**
187
256
  * Open an ephemeral SSE stream with optional filtering.
188
257
  * No session, no replay, no ACK.
258
+ *
259
+ * Accepts either a Filter (for backwards compatibility) or
260
+ * SubscribeOptions for additional control (decode, signal).
189
261
  */
190
- subscribe(filter?: Filter, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
262
+ subscribe(filterOrOptions?: Filter | SubscribeOptions, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
191
263
  /** Fetch the last-seen decoded values for each (device, PGN) pair. */
192
264
  decodedValues(filter?: Filter, signal?: AbortSignal): Promise<DecodedDeviceValues[]>;
193
265
  /** Transmit a CAN frame through the server. */
194
266
  send(params: SendParams, signal?: AbortSignal): Promise<void>;
195
267
  /**
196
268
  * Send an ISO Request (PGN 59904) and wait for the response frame.
197
- * Returns the response frame, or throws HttpError with status 504 on timeout.
269
+ * Returns the response frame, or throws HttpError on timeout (408).
198
270
  */
199
271
  query(params: QueryParams, signal?: AbortSignal): Promise<Frame>;
200
- /** Check server health. */
272
+ /**
273
+ * Query historical frames (requires journaling on the server).
274
+ * Returns an array of frames matching the query parameters.
275
+ */
276
+ history(params: HistoryParams, signal?: AbortSignal): Promise<Frame[]>;
277
+ /** Check server health (GET /healthz). */
201
278
  health(signal?: AbortSignal): Promise<HealthStatus>;
279
+ /** Liveness probe (GET /livez). */
280
+ liveness(signal?: AbortSignal): Promise<HealthStatus>;
281
+ /** Readiness probe (GET /readyz). */
282
+ readiness(signal?: AbortSignal): Promise<HealthStatus>;
202
283
  /** Fetch boat-side replication status (only available when replication is configured). */
203
284
  replicationStatus(signal?: AbortSignal): Promise<ReplicationStatus>;
204
285
  /** Create or reconnect a buffered session on the server. */
@@ -230,6 +311,12 @@ declare class CloudClient {
230
311
  status(instanceId: string, signal?: AbortSignal): Promise<InstanceStatus>;
231
312
  /** Fetch recent replication diagnostic events for an instance. */
232
313
  replicationEvents(instanceId: string, limit?: number, signal?: AbortSignal): Promise<ReplicationEvent[]>;
314
+ /** Check cloud server health (GET /healthz). */
315
+ health(signal?: AbortSignal): Promise<HealthStatus>;
316
+ /** Liveness probe (GET /livez). */
317
+ liveness(signal?: AbortSignal): Promise<HealthStatus>;
318
+ /** Readiness probe (GET /readyz). */
319
+ readiness(signal?: AbortSignal): Promise<HealthStatus>;
233
320
  }
234
321
 
235
322
  declare class LplexError extends Error {
@@ -241,4 +328,4 @@ declare class HttpError extends LplexError {
241
328
  constructor(method: string, path: string, status: number, body: string);
242
329
  }
243
330
 
244
- export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type DecodedDeviceValues, type DecodedPGNValue, type Device, type DeviceValues, type Event, type Filter, type Frame, type HealthStatus, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type QueryParams, type ReplicationEvent, type ReplicationEventType, type ReplicationStatus, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
331
+ export { type BrokerHealth, Client, type ClientOptions, CloudClient, type CloudClientOptions, type DecodedDeviceValues, type DecodedPGNValue, type Device, type DeviceRemoved, type DeviceValues, type Event, type Filter, type Frame, type HealthStatus, type HistoryParams, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type QueryParams, type ReplicationEvent, type ReplicationEventType, type ReplicationHealth, type ReplicationStatus, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo, type SubscribeOptions };
package/dist/index.js CHANGED
@@ -59,8 +59,16 @@ async function* parseSSE(body) {
59
59
  }
60
60
  }
61
61
  function classify(obj) {
62
- if ("type" in obj && obj.type === "device") {
63
- return { type: "device", device: obj };
62
+ if ("type" in obj) {
63
+ if (obj.type === "device") {
64
+ return { type: "device", device: obj };
65
+ }
66
+ if (obj.type === "device_removed") {
67
+ return {
68
+ type: "device_removed",
69
+ deviceRemoved: obj
70
+ };
71
+ }
64
72
  }
65
73
  if ("seq" in obj) {
66
74
  return { type: "frame", frame: obj };
@@ -155,14 +163,27 @@ var Client = class {
155
163
  /**
156
164
  * Open an ephemeral SSE stream with optional filtering.
157
165
  * No session, no replay, no ACK.
166
+ *
167
+ * Accepts either a Filter (for backwards compatibility) or
168
+ * SubscribeOptions for additional control (decode, signal).
158
169
  */
159
- async subscribe(filter, signal) {
170
+ async subscribe(filterOrOptions, signal) {
171
+ let filter;
172
+ let decode;
173
+ let sig = signal;
174
+ if (filterOrOptions && isSubscribeOptions(filterOrOptions)) {
175
+ filter = filterOrOptions.filter;
176
+ decode = filterOrOptions.decode;
177
+ sig = filterOrOptions.signal ?? sig;
178
+ } else {
179
+ filter = filterOrOptions;
180
+ }
160
181
  let url = `${this.#baseURL}/events`;
161
- const qs = filterToQueryString(filter);
182
+ const qs = filterToQueryString(filter, decode);
162
183
  if (qs) url += `?${qs}`;
163
184
  const resp = await this.#fetch(url, {
164
185
  headers: { Accept: "text/event-stream" },
165
- signal
186
+ signal: sig
166
187
  });
167
188
  if (!resp.ok) {
168
189
  const body = await resp.text();
@@ -201,7 +222,7 @@ var Client = class {
201
222
  }
202
223
  /**
203
224
  * Send an ISO Request (PGN 59904) and wait for the response frame.
204
- * Returns the response frame, or throws HttpError with status 504 on timeout.
225
+ * Returns the response frame, or throws HttpError on timeout (408).
205
226
  */
206
227
  async query(params, signal) {
207
228
  const url = `${this.#baseURL}/query`;
@@ -217,7 +238,28 @@ var Client = class {
217
238
  }
218
239
  return resp.json();
219
240
  }
220
- /** Check server health. */
241
+ /**
242
+ * Query historical frames (requires journaling on the server).
243
+ * Returns an array of frames matching the query parameters.
244
+ */
245
+ async history(params, signal) {
246
+ const qs = new URLSearchParams();
247
+ qs.set("from", params.from);
248
+ if (params.to) qs.set("to", params.to);
249
+ for (const p of params.pgn ?? []) qs.append("pgn", p.toString());
250
+ for (const s of params.src ?? []) qs.append("src", s.toString());
251
+ if (params.limit !== void 0) qs.set("limit", params.limit.toString());
252
+ if (params.interval) qs.set("interval", params.interval);
253
+ if (params.decode) qs.set("decode", "true");
254
+ const url = `${this.#baseURL}/history?${qs.toString()}`;
255
+ const resp = await this.#fetch(url, { signal });
256
+ if (!resp.ok) {
257
+ const body = await resp.text();
258
+ throw new HttpError("GET", url, resp.status, body);
259
+ }
260
+ return resp.json();
261
+ }
262
+ /** Check server health (GET /healthz). */
221
263
  async health(signal) {
222
264
  const url = `${this.#baseURL}/healthz`;
223
265
  const resp = await this.#fetch(url, { signal });
@@ -227,6 +269,26 @@ var Client = class {
227
269
  }
228
270
  return resp.json();
229
271
  }
272
+ /** Liveness probe (GET /livez). */
273
+ async liveness(signal) {
274
+ const url = `${this.#baseURL}/livez`;
275
+ const resp = await this.#fetch(url, { signal });
276
+ if (!resp.ok) {
277
+ const body = await resp.text();
278
+ throw new HttpError("GET", url, resp.status, body);
279
+ }
280
+ return resp.json();
281
+ }
282
+ /** Readiness probe (GET /readyz). */
283
+ async readiness(signal) {
284
+ const url = `${this.#baseURL}/readyz`;
285
+ const resp = await this.#fetch(url, { signal });
286
+ if (!resp.ok) {
287
+ const body = await resp.text();
288
+ throw new HttpError("GET", url, resp.status, body);
289
+ }
290
+ return resp.json();
291
+ }
230
292
  /** Fetch boat-side replication status (only available when replication is configured). */
231
293
  async replicationStatus(signal) {
232
294
  const url = `${this.#baseURL}/replication/status`;
@@ -260,19 +322,26 @@ var Client = class {
260
322
  return new Session(this.#baseURL, this.#fetch, info);
261
323
  }
262
324
  };
325
+ function isSubscribeOptions(obj) {
326
+ return "decode" in obj || "signal" in obj || "filter" in obj;
327
+ }
263
328
  function filterIsEmpty(f) {
264
- return !f.pgn?.length && !f.exclude_pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length && !f.exclude_name?.length;
329
+ return !f.pgn?.length && !f.exclude_pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length && !f.exclude_name?.length && !f.bus?.length;
265
330
  }
266
- function filterToQueryString(f) {
267
- if (!f || filterIsEmpty(f)) return "";
331
+ function filterToQueryString(f, decode) {
332
+ if ((!f || filterIsEmpty(f)) && !decode) return "";
268
333
  const params = new URLSearchParams();
269
- for (const p of f.pgn ?? []) params.append("pgn", p.toString());
270
- for (const p of f.exclude_pgn ?? [])
271
- params.append("exclude_pgn", p.toString());
272
- for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
273
- for (const i of f.instance ?? []) params.append("instance", i.toString());
274
- for (const n of f.name ?? []) params.append("name", n);
275
- for (const n of f.exclude_name ?? []) params.append("exclude_name", n);
334
+ if (f) {
335
+ for (const p of f.pgn ?? []) params.append("pgn", p.toString());
336
+ for (const p of f.exclude_pgn ?? [])
337
+ params.append("exclude_pgn", p.toString());
338
+ for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
339
+ for (const i of f.instance ?? []) params.append("instance", i.toString());
340
+ for (const n of f.name ?? []) params.append("name", n);
341
+ for (const n of f.exclude_name ?? []) params.append("exclude_name", n);
342
+ for (const b of f.bus ?? []) params.append("bus", b);
343
+ }
344
+ if (decode) params.set("decode", "true");
276
345
  return params.toString();
277
346
  }
278
347
  function filterToJSON(f) {
@@ -283,6 +352,7 @@ function filterToJSON(f) {
283
352
  if (f.instance?.length) m.instance = f.instance;
284
353
  if (f.name?.length) m.name = f.name;
285
354
  if (f.exclude_name?.length) m.exclude_name = f.exclude_name;
355
+ if (f.bus?.length) m.bus = f.bus;
286
356
  return m;
287
357
  }
288
358
 
@@ -338,6 +408,36 @@ var CloudClient = class {
338
408
  }
339
409
  return resp.json();
340
410
  }
411
+ /** Check cloud server health (GET /healthz). */
412
+ async health(signal) {
413
+ const url = `${this.#baseURL}/healthz`;
414
+ const resp = await this.#fetch(url, { signal });
415
+ if (!resp.ok) {
416
+ const body = await resp.text();
417
+ throw new HttpError("GET", url, resp.status, body);
418
+ }
419
+ return resp.json();
420
+ }
421
+ /** Liveness probe (GET /livez). */
422
+ async liveness(signal) {
423
+ const url = `${this.#baseURL}/livez`;
424
+ const resp = await this.#fetch(url, { signal });
425
+ if (!resp.ok) {
426
+ const body = await resp.text();
427
+ throw new HttpError("GET", url, resp.status, body);
428
+ }
429
+ return resp.json();
430
+ }
431
+ /** Readiness probe (GET /readyz). */
432
+ async readiness(signal) {
433
+ const url = `${this.#baseURL}/readyz`;
434
+ const resp = await this.#fetch(url, { signal });
435
+ if (!resp.ok) {
436
+ const body = await resp.text();
437
+ throw new HttpError("GET", url, resp.status, body);
438
+ }
439
+ return resp.json();
440
+ }
341
441
  };
342
442
  export {
343
443
  Client,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixfathoms/lplex",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript client for lplex CAN bus HTTP bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",