@sixfathoms/lplex 0.0.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 ADDED
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Client: () => Client,
24
+ HttpError: () => HttpError,
25
+ LplexError: () => LplexError,
26
+ Session: () => Session
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/errors.ts
31
+ var LplexError = class extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "LplexError";
35
+ }
36
+ };
37
+ var HttpError = class extends LplexError {
38
+ status;
39
+ body;
40
+ constructor(method, path, status, body) {
41
+ super(`${method} ${path} returned ${status}: ${body}`);
42
+ this.name = "HttpError";
43
+ this.status = status;
44
+ this.body = body;
45
+ }
46
+ };
47
+
48
+ // src/sse.ts
49
+ async function* parseSSE(body) {
50
+ const reader = body.getReader();
51
+ const decoder = new TextDecoder();
52
+ let buffer = "";
53
+ try {
54
+ for (; ; ) {
55
+ const { done, value } = await reader.read();
56
+ if (done) break;
57
+ buffer += decoder.decode(value, { stream: true });
58
+ for (let newlineIdx = buffer.indexOf("\n"); newlineIdx !== -1; newlineIdx = buffer.indexOf("\n")) {
59
+ const line = buffer.slice(0, newlineIdx);
60
+ buffer = buffer.slice(newlineIdx + 1);
61
+ if (!line.startsWith("data: ")) continue;
62
+ const json = line.slice(6);
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(json);
66
+ } catch {
67
+ continue;
68
+ }
69
+ if (typeof parsed !== "object" || parsed === null) continue;
70
+ const event = classify(parsed);
71
+ if (event) yield event;
72
+ }
73
+ }
74
+ buffer += decoder.decode();
75
+ if (buffer.startsWith("data: ")) {
76
+ const json = buffer.slice(6);
77
+ try {
78
+ const parsed = JSON.parse(json);
79
+ if (typeof parsed === "object" && parsed !== null) {
80
+ const event = classify(parsed);
81
+ if (event) yield event;
82
+ }
83
+ } catch {
84
+ }
85
+ }
86
+ } finally {
87
+ reader.releaseLock();
88
+ }
89
+ }
90
+ function classify(obj) {
91
+ if ("type" in obj && obj.type === "device") {
92
+ return { type: "device", device: obj };
93
+ }
94
+ if ("seq" in obj) {
95
+ return { type: "frame", frame: obj };
96
+ }
97
+ return null;
98
+ }
99
+
100
+ // src/session.ts
101
+ var Session = class {
102
+ #baseURL;
103
+ #fetch;
104
+ #info;
105
+ #lastAckedSeq = 0;
106
+ /** @internal Created by Client.createSession, not for direct use. */
107
+ constructor(baseURL, fetchFn, info) {
108
+ this.#baseURL = baseURL;
109
+ this.#fetch = fetchFn;
110
+ this.#info = info;
111
+ }
112
+ get info() {
113
+ return this.#info;
114
+ }
115
+ get lastAckedSeq() {
116
+ return this.#lastAckedSeq;
117
+ }
118
+ /**
119
+ * Open the SSE stream for this session. Replays buffered frames
120
+ * from the cursor, then streams live.
121
+ */
122
+ async subscribe(signal) {
123
+ const url = `${this.#baseURL}/clients/${this.#info.client_id}/events`;
124
+ const resp = await this.#fetch(url, {
125
+ headers: { Accept: "text/event-stream" },
126
+ signal
127
+ });
128
+ if (!resp.ok) {
129
+ const body = await resp.text();
130
+ throw new HttpError("GET", url, resp.status, body);
131
+ }
132
+ if (!resp.body) {
133
+ throw new HttpError("GET", url, resp.status, "no response body");
134
+ }
135
+ return parseSSE(resp.body);
136
+ }
137
+ /** Advance the cursor for this session to the given sequence number. */
138
+ async ack(seq, signal) {
139
+ const url = `${this.#baseURL}/clients/${this.#info.client_id}/ack`;
140
+ const resp = await this.#fetch(url, {
141
+ method: "PUT",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ seq }),
144
+ signal
145
+ });
146
+ if (resp.status !== 204) {
147
+ const body = await resp.text();
148
+ throw new HttpError("PUT", url, resp.status, body);
149
+ }
150
+ this.#lastAckedSeq = seq;
151
+ }
152
+ };
153
+
154
+ // src/client.ts
155
+ var Client = class {
156
+ #baseURL;
157
+ #fetch;
158
+ constructor(baseURL, options) {
159
+ this.#baseURL = baseURL.replace(/\/+$/, "");
160
+ this.#fetch = options?.fetch ?? globalThis.fetch.bind(globalThis);
161
+ }
162
+ /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
163
+ async devices(signal) {
164
+ const url = `${this.#baseURL}/devices`;
165
+ const resp = await this.#fetch(url, { signal });
166
+ if (!resp.ok) {
167
+ const body = await resp.text();
168
+ throw new HttpError("GET", url, resp.status, body);
169
+ }
170
+ return resp.json();
171
+ }
172
+ /**
173
+ * Open an ephemeral SSE stream with optional filtering.
174
+ * No session, no replay, no ACK.
175
+ */
176
+ async subscribe(filter, signal) {
177
+ let url = `${this.#baseURL}/events`;
178
+ const qs = filterToQueryString(filter);
179
+ if (qs) url += `?${qs}`;
180
+ const resp = await this.#fetch(url, {
181
+ headers: { Accept: "text/event-stream" },
182
+ signal
183
+ });
184
+ if (!resp.ok) {
185
+ const body = await resp.text();
186
+ throw new HttpError("GET", url, resp.status, body);
187
+ }
188
+ if (!resp.body) {
189
+ throw new HttpError("GET", url, resp.status, "no response body");
190
+ }
191
+ return parseSSE(resp.body);
192
+ }
193
+ /** Transmit a CAN frame through the server. */
194
+ async send(params, signal) {
195
+ const url = `${this.#baseURL}/send`;
196
+ const resp = await this.#fetch(url, {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify(params),
200
+ signal
201
+ });
202
+ if (resp.status !== 202) {
203
+ const body = await resp.text();
204
+ throw new HttpError("POST", url, resp.status, body);
205
+ }
206
+ }
207
+ /** Create or reconnect a buffered session on the server. */
208
+ async createSession(config, signal) {
209
+ const url = `${this.#baseURL}/clients/${config.clientId}`;
210
+ const putBody = {
211
+ buffer_timeout: config.bufferTimeout
212
+ };
213
+ if (config.filter && !filterIsEmpty(config.filter)) {
214
+ putBody.filter = filterToJSON(config.filter);
215
+ }
216
+ const resp = await this.#fetch(url, {
217
+ method: "PUT",
218
+ headers: { "Content-Type": "application/json" },
219
+ body: JSON.stringify(putBody),
220
+ signal
221
+ });
222
+ if (!resp.ok) {
223
+ const body = await resp.text();
224
+ throw new HttpError("PUT", url, resp.status, body);
225
+ }
226
+ const info = await resp.json();
227
+ return new Session(this.#baseURL, this.#fetch, info);
228
+ }
229
+ };
230
+ function filterIsEmpty(f) {
231
+ return !f.pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length;
232
+ }
233
+ function filterToQueryString(f) {
234
+ if (!f || filterIsEmpty(f)) return "";
235
+ const params = new URLSearchParams();
236
+ for (const p of f.pgn ?? []) params.append("pgn", p.toString());
237
+ for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
238
+ for (const i of f.instance ?? []) params.append("instance", i.toString());
239
+ for (const n of f.name ?? []) params.append("name", n);
240
+ return params.toString();
241
+ }
242
+ function filterToJSON(f) {
243
+ const m = {};
244
+ if (f.pgn?.length) m.pgn = f.pgn;
245
+ if (f.manufacturer?.length) m.manufacturer = f.manufacturer;
246
+ if (f.instance?.length) m.instance = f.instance;
247
+ if (f.name?.length) m.name = f.name;
248
+ return m;
249
+ }
250
+ // Annotate the CommonJS export names for ESM import in node:
251
+ 0 && (module.exports = {
252
+ Client,
253
+ HttpError,
254
+ LplexError,
255
+ Session
256
+ });
@@ -0,0 +1,116 @@
1
+ /** A single CAN frame received from the lplex server. */
2
+ interface Frame {
3
+ seq: number;
4
+ ts: string;
5
+ prio: number;
6
+ pgn: number;
7
+ src: number;
8
+ dst: number;
9
+ data: string;
10
+ }
11
+ /** An NMEA 2000 device discovered on the bus. */
12
+ interface Device {
13
+ src: number;
14
+ name: string;
15
+ manufacturer: string;
16
+ manufacturer_code: number;
17
+ device_class: number;
18
+ device_function: number;
19
+ device_instance: number;
20
+ unique_number: number;
21
+ model_id: string;
22
+ software_version: string;
23
+ model_version: string;
24
+ model_serial: string;
25
+ product_code: number;
26
+ first_seen: string;
27
+ last_seen: string;
28
+ packet_count: number;
29
+ byte_count: number;
30
+ }
31
+ /** Discriminated union for SSE events. */
32
+ type Event = {
33
+ type: "frame";
34
+ frame: Frame;
35
+ } | {
36
+ type: "device";
37
+ device: Device;
38
+ };
39
+ /**
40
+ * Filter for CAN frames.
41
+ * Categories are AND'd, values within a category are OR'd.
42
+ */
43
+ interface Filter {
44
+ pgn?: number[];
45
+ manufacturer?: string[];
46
+ instance?: number[];
47
+ name?: string[];
48
+ }
49
+ /** Configuration for creating a buffered session. */
50
+ interface SessionConfig {
51
+ clientId: string;
52
+ bufferTimeout: string;
53
+ filter?: Filter;
54
+ }
55
+ /** Server response from creating or reconnecting a session. */
56
+ interface SessionInfo {
57
+ client_id: string;
58
+ seq: number;
59
+ cursor: number;
60
+ devices: Device[];
61
+ }
62
+ /** Parameters for transmitting a CAN frame. */
63
+ interface SendParams {
64
+ pgn: number;
65
+ src: number;
66
+ dst: number;
67
+ prio: number;
68
+ data: string;
69
+ }
70
+
71
+ type FetchFn$1 = typeof globalThis.fetch;
72
+ declare class Session {
73
+ #private;
74
+ /** @internal Created by Client.createSession, not for direct use. */
75
+ constructor(baseURL: string, fetchFn: FetchFn$1, info: SessionInfo);
76
+ get info(): SessionInfo;
77
+ get lastAckedSeq(): number;
78
+ /**
79
+ * Open the SSE stream for this session. Replays buffered frames
80
+ * from the cursor, then streams live.
81
+ */
82
+ subscribe(signal?: AbortSignal): Promise<AsyncIterable<Event>>;
83
+ /** Advance the cursor for this session to the given sequence number. */
84
+ ack(seq: number, signal?: AbortSignal): Promise<void>;
85
+ }
86
+
87
+ type FetchFn = typeof globalThis.fetch;
88
+ interface ClientOptions {
89
+ fetch?: FetchFn;
90
+ }
91
+ declare class Client {
92
+ #private;
93
+ constructor(baseURL: string, options?: ClientOptions);
94
+ /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
95
+ devices(signal?: AbortSignal): Promise<Device[]>;
96
+ /**
97
+ * Open an ephemeral SSE stream with optional filtering.
98
+ * No session, no replay, no ACK.
99
+ */
100
+ subscribe(filter?: Filter, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
101
+ /** Transmit a CAN frame through the server. */
102
+ send(params: SendParams, signal?: AbortSignal): Promise<void>;
103
+ /** Create or reconnect a buffered session on the server. */
104
+ createSession(config: SessionConfig, signal?: AbortSignal): Promise<Session>;
105
+ }
106
+
107
+ declare class LplexError extends Error {
108
+ constructor(message: string);
109
+ }
110
+ declare class HttpError extends LplexError {
111
+ readonly status: number;
112
+ readonly body: string;
113
+ constructor(method: string, path: string, status: number, body: string);
114
+ }
115
+
116
+ export { Client, type ClientOptions, type Device, type Event, type Filter, type Frame, HttpError, LplexError, type SendParams, Session, type SessionConfig, type SessionInfo };
@@ -0,0 +1,116 @@
1
+ /** A single CAN frame received from the lplex server. */
2
+ interface Frame {
3
+ seq: number;
4
+ ts: string;
5
+ prio: number;
6
+ pgn: number;
7
+ src: number;
8
+ dst: number;
9
+ data: string;
10
+ }
11
+ /** An NMEA 2000 device discovered on the bus. */
12
+ interface Device {
13
+ src: number;
14
+ name: string;
15
+ manufacturer: string;
16
+ manufacturer_code: number;
17
+ device_class: number;
18
+ device_function: number;
19
+ device_instance: number;
20
+ unique_number: number;
21
+ model_id: string;
22
+ software_version: string;
23
+ model_version: string;
24
+ model_serial: string;
25
+ product_code: number;
26
+ first_seen: string;
27
+ last_seen: string;
28
+ packet_count: number;
29
+ byte_count: number;
30
+ }
31
+ /** Discriminated union for SSE events. */
32
+ type Event = {
33
+ type: "frame";
34
+ frame: Frame;
35
+ } | {
36
+ type: "device";
37
+ device: Device;
38
+ };
39
+ /**
40
+ * Filter for CAN frames.
41
+ * Categories are AND'd, values within a category are OR'd.
42
+ */
43
+ interface Filter {
44
+ pgn?: number[];
45
+ manufacturer?: string[];
46
+ instance?: number[];
47
+ name?: string[];
48
+ }
49
+ /** Configuration for creating a buffered session. */
50
+ interface SessionConfig {
51
+ clientId: string;
52
+ bufferTimeout: string;
53
+ filter?: Filter;
54
+ }
55
+ /** Server response from creating or reconnecting a session. */
56
+ interface SessionInfo {
57
+ client_id: string;
58
+ seq: number;
59
+ cursor: number;
60
+ devices: Device[];
61
+ }
62
+ /** Parameters for transmitting a CAN frame. */
63
+ interface SendParams {
64
+ pgn: number;
65
+ src: number;
66
+ dst: number;
67
+ prio: number;
68
+ data: string;
69
+ }
70
+
71
+ type FetchFn$1 = typeof globalThis.fetch;
72
+ declare class Session {
73
+ #private;
74
+ /** @internal Created by Client.createSession, not for direct use. */
75
+ constructor(baseURL: string, fetchFn: FetchFn$1, info: SessionInfo);
76
+ get info(): SessionInfo;
77
+ get lastAckedSeq(): number;
78
+ /**
79
+ * Open the SSE stream for this session. Replays buffered frames
80
+ * from the cursor, then streams live.
81
+ */
82
+ subscribe(signal?: AbortSignal): Promise<AsyncIterable<Event>>;
83
+ /** Advance the cursor for this session to the given sequence number. */
84
+ ack(seq: number, signal?: AbortSignal): Promise<void>;
85
+ }
86
+
87
+ type FetchFn = typeof globalThis.fetch;
88
+ interface ClientOptions {
89
+ fetch?: FetchFn;
90
+ }
91
+ declare class Client {
92
+ #private;
93
+ constructor(baseURL: string, options?: ClientOptions);
94
+ /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
95
+ devices(signal?: AbortSignal): Promise<Device[]>;
96
+ /**
97
+ * Open an ephemeral SSE stream with optional filtering.
98
+ * No session, no replay, no ACK.
99
+ */
100
+ subscribe(filter?: Filter, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
101
+ /** Transmit a CAN frame through the server. */
102
+ send(params: SendParams, signal?: AbortSignal): Promise<void>;
103
+ /** Create or reconnect a buffered session on the server. */
104
+ createSession(config: SessionConfig, signal?: AbortSignal): Promise<Session>;
105
+ }
106
+
107
+ declare class LplexError extends Error {
108
+ constructor(message: string);
109
+ }
110
+ declare class HttpError extends LplexError {
111
+ readonly status: number;
112
+ readonly body: string;
113
+ constructor(method: string, path: string, status: number, body: string);
114
+ }
115
+
116
+ export { Client, type ClientOptions, type Device, type Event, type Filter, type Frame, HttpError, LplexError, type SendParams, Session, type SessionConfig, type SessionInfo };
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ // src/errors.ts
2
+ var LplexError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "LplexError";
6
+ }
7
+ };
8
+ var HttpError = class extends LplexError {
9
+ status;
10
+ body;
11
+ constructor(method, path, status, body) {
12
+ super(`${method} ${path} returned ${status}: ${body}`);
13
+ this.name = "HttpError";
14
+ this.status = status;
15
+ this.body = body;
16
+ }
17
+ };
18
+
19
+ // src/sse.ts
20
+ async function* parseSSE(body) {
21
+ const reader = body.getReader();
22
+ const decoder = new TextDecoder();
23
+ let buffer = "";
24
+ try {
25
+ for (; ; ) {
26
+ const { done, value } = await reader.read();
27
+ if (done) break;
28
+ buffer += decoder.decode(value, { stream: true });
29
+ for (let newlineIdx = buffer.indexOf("\n"); newlineIdx !== -1; newlineIdx = buffer.indexOf("\n")) {
30
+ const line = buffer.slice(0, newlineIdx);
31
+ buffer = buffer.slice(newlineIdx + 1);
32
+ if (!line.startsWith("data: ")) continue;
33
+ const json = line.slice(6);
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(json);
37
+ } catch {
38
+ continue;
39
+ }
40
+ if (typeof parsed !== "object" || parsed === null) continue;
41
+ const event = classify(parsed);
42
+ if (event) yield event;
43
+ }
44
+ }
45
+ buffer += decoder.decode();
46
+ if (buffer.startsWith("data: ")) {
47
+ const json = buffer.slice(6);
48
+ try {
49
+ const parsed = JSON.parse(json);
50
+ if (typeof parsed === "object" && parsed !== null) {
51
+ const event = classify(parsed);
52
+ if (event) yield event;
53
+ }
54
+ } catch {
55
+ }
56
+ }
57
+ } finally {
58
+ reader.releaseLock();
59
+ }
60
+ }
61
+ function classify(obj) {
62
+ if ("type" in obj && obj.type === "device") {
63
+ return { type: "device", device: obj };
64
+ }
65
+ if ("seq" in obj) {
66
+ return { type: "frame", frame: obj };
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // src/session.ts
72
+ var Session = class {
73
+ #baseURL;
74
+ #fetch;
75
+ #info;
76
+ #lastAckedSeq = 0;
77
+ /** @internal Created by Client.createSession, not for direct use. */
78
+ constructor(baseURL, fetchFn, info) {
79
+ this.#baseURL = baseURL;
80
+ this.#fetch = fetchFn;
81
+ this.#info = info;
82
+ }
83
+ get info() {
84
+ return this.#info;
85
+ }
86
+ get lastAckedSeq() {
87
+ return this.#lastAckedSeq;
88
+ }
89
+ /**
90
+ * Open the SSE stream for this session. Replays buffered frames
91
+ * from the cursor, then streams live.
92
+ */
93
+ async subscribe(signal) {
94
+ const url = `${this.#baseURL}/clients/${this.#info.client_id}/events`;
95
+ const resp = await this.#fetch(url, {
96
+ headers: { Accept: "text/event-stream" },
97
+ signal
98
+ });
99
+ if (!resp.ok) {
100
+ const body = await resp.text();
101
+ throw new HttpError("GET", url, resp.status, body);
102
+ }
103
+ if (!resp.body) {
104
+ throw new HttpError("GET", url, resp.status, "no response body");
105
+ }
106
+ return parseSSE(resp.body);
107
+ }
108
+ /** Advance the cursor for this session to the given sequence number. */
109
+ async ack(seq, signal) {
110
+ const url = `${this.#baseURL}/clients/${this.#info.client_id}/ack`;
111
+ const resp = await this.#fetch(url, {
112
+ method: "PUT",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({ seq }),
115
+ signal
116
+ });
117
+ if (resp.status !== 204) {
118
+ const body = await resp.text();
119
+ throw new HttpError("PUT", url, resp.status, body);
120
+ }
121
+ this.#lastAckedSeq = seq;
122
+ }
123
+ };
124
+
125
+ // src/client.ts
126
+ var Client = class {
127
+ #baseURL;
128
+ #fetch;
129
+ constructor(baseURL, options) {
130
+ this.#baseURL = baseURL.replace(/\/+$/, "");
131
+ this.#fetch = options?.fetch ?? globalThis.fetch.bind(globalThis);
132
+ }
133
+ /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
134
+ async devices(signal) {
135
+ const url = `${this.#baseURL}/devices`;
136
+ const resp = await this.#fetch(url, { signal });
137
+ if (!resp.ok) {
138
+ const body = await resp.text();
139
+ throw new HttpError("GET", url, resp.status, body);
140
+ }
141
+ return resp.json();
142
+ }
143
+ /**
144
+ * Open an ephemeral SSE stream with optional filtering.
145
+ * No session, no replay, no ACK.
146
+ */
147
+ async subscribe(filter, signal) {
148
+ let url = `${this.#baseURL}/events`;
149
+ const qs = filterToQueryString(filter);
150
+ if (qs) url += `?${qs}`;
151
+ const resp = await this.#fetch(url, {
152
+ headers: { Accept: "text/event-stream" },
153
+ signal
154
+ });
155
+ if (!resp.ok) {
156
+ const body = await resp.text();
157
+ throw new HttpError("GET", url, resp.status, body);
158
+ }
159
+ if (!resp.body) {
160
+ throw new HttpError("GET", url, resp.status, "no response body");
161
+ }
162
+ return parseSSE(resp.body);
163
+ }
164
+ /** Transmit a CAN frame through the server. */
165
+ async send(params, signal) {
166
+ const url = `${this.#baseURL}/send`;
167
+ const resp = await this.#fetch(url, {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/json" },
170
+ body: JSON.stringify(params),
171
+ signal
172
+ });
173
+ if (resp.status !== 202) {
174
+ const body = await resp.text();
175
+ throw new HttpError("POST", url, resp.status, body);
176
+ }
177
+ }
178
+ /** Create or reconnect a buffered session on the server. */
179
+ async createSession(config, signal) {
180
+ const url = `${this.#baseURL}/clients/${config.clientId}`;
181
+ const putBody = {
182
+ buffer_timeout: config.bufferTimeout
183
+ };
184
+ if (config.filter && !filterIsEmpty(config.filter)) {
185
+ putBody.filter = filterToJSON(config.filter);
186
+ }
187
+ const resp = await this.#fetch(url, {
188
+ method: "PUT",
189
+ headers: { "Content-Type": "application/json" },
190
+ body: JSON.stringify(putBody),
191
+ signal
192
+ });
193
+ if (!resp.ok) {
194
+ const body = await resp.text();
195
+ throw new HttpError("PUT", url, resp.status, body);
196
+ }
197
+ const info = await resp.json();
198
+ return new Session(this.#baseURL, this.#fetch, info);
199
+ }
200
+ };
201
+ function filterIsEmpty(f) {
202
+ return !f.pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length;
203
+ }
204
+ function filterToQueryString(f) {
205
+ if (!f || filterIsEmpty(f)) return "";
206
+ const params = new URLSearchParams();
207
+ for (const p of f.pgn ?? []) params.append("pgn", p.toString());
208
+ for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
209
+ for (const i of f.instance ?? []) params.append("instance", i.toString());
210
+ for (const n of f.name ?? []) params.append("name", n);
211
+ return params.toString();
212
+ }
213
+ function filterToJSON(f) {
214
+ const m = {};
215
+ if (f.pgn?.length) m.pgn = f.pgn;
216
+ if (f.manufacturer?.length) m.manufacturer = f.manufacturer;
217
+ if (f.instance?.length) m.instance = f.instance;
218
+ if (f.name?.length) m.name = f.name;
219
+ return m;
220
+ }
221
+ export {
222
+ Client,
223
+ HttpError,
224
+ LplexError,
225
+ Session
226
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@sixfathoms/lplex",
3
+ "version": "0.0.0",
4
+ "description": "TypeScript client for lplex CAN bus HTTP bridge",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "test": "vitest run",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.5.0",
32
+ "vitest": "^2.0.0"
33
+ },
34
+ "license": "MIT"
35
+ }