diff --git a/CLAUDE.md b/CLAUDE.md index e13b623..2553b25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ All types use `snake_case` field names matching the server's JSON output exactly | `/clients/{id}/ack` | PUT | ACK sequence number. JSON body: `{ "seq": N }`. Returns 204. | | `/send` | POST | Transmit CAN frame. JSON body: `pgn`, `src`, `dst`, `prio`, `data`. Returns 202. | | `/devices` | GET | Device snapshot. Returns JSON array. | -| `/values` | GET | Last-seen value per (device, PGN). Returns JSON array grouped by device. | +| `/values` | GET | Last-seen value per (device, PGN). Query params: `pgn`, `manufacturer`, `instance`, `name` (repeatable). Returns JSON array grouped by device. | ## Release diff --git a/packages/lplex/README.md b/packages/lplex/README.md index ae35b5f..01d70bc 100644 --- a/packages/lplex/README.md +++ b/packages/lplex/README.md @@ -58,7 +58,7 @@ for (const d of devices) { } ``` -### `client.values(signal?): Promise` +### `client.values(filter?, signal?): Promise` 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. @@ -72,6 +72,15 @@ for (const device of snapshot) { } ``` +Pass a `Filter` to narrow results by PGN and/or device criteria: + +```typescript +const positions = await client.values({ + pgn: [129025], + manufacturer: ["Garmin"], +}); +``` + ### `client.subscribe(filter?, signal?): Promise>` Opens an ephemeral SSE stream. No session state, no replay. Frames flow until you stop reading or abort. @@ -326,7 +335,7 @@ interface DeviceValues { | `/clients/{id}/ack` | PUT | ACK sequence number. JSON body: `{ "seq": N }`. Returns 204. | | `/send` | POST | Transmit CAN frame. JSON body: `pgn`, `src`, `dst`, `prio`, `data`. Returns 202. | | `/devices` | GET | Device snapshot. Returns JSON array. | -| `/values` | GET | Last-seen value per (device, PGN). Returns JSON array grouped by device. | +| `/values` | GET | Last-seen value per (device, PGN). Query params: `pgn`, `manufacturer`, `instance`, `name` (repeatable). Returns JSON array grouped by device. | ## License diff --git a/packages/lplex/src/client.ts b/packages/lplex/src/client.ts index 9f4e75e..5330cf3 100644 --- a/packages/lplex/src/client.ts +++ b/packages/lplex/src/client.ts @@ -40,8 +40,10 @@ export class Client { } /** Fetch the last-seen value for each (device, PGN) pair. */ - async values(signal?: AbortSignal): Promise { - const url = `${this.#baseURL}/values`; + async values(filter?: Filter, signal?: AbortSignal): Promise { + let url = `${this.#baseURL}/values`; + const qs = filterToQueryString(filter); + if (qs) url += `?${qs}`; const resp = await this.#fetch(url, { signal }); if (!resp.ok) { diff --git a/packages/lplex/test/client.test.ts b/packages/lplex/test/client.test.ts index 20fc5d3..ed8ce1b 100644 --- a/packages/lplex/test/client.test.ts +++ b/packages/lplex/test/client.test.ts @@ -102,6 +102,51 @@ describe("Client.values", () => { expect(result).toEqual([]); }); + it("encodes filter as query params", async () => { + let capturedURL = ""; + const mockFetch = async (url: string | URL | Request) => { + capturedURL = String(url); + return jsonResponse([]); + }; + + const client = new Client("http://localhost:8089", { + fetch: mockFetch as typeof fetch, + }); + await client.values({ pgn: [129025, 129026], manufacturer: ["Garmin"] }); + + const parsed = new URL(capturedURL); + expect(parsed.searchParams.getAll("pgn")).toEqual(["129025", "129026"]); + expect(parsed.searchParams.getAll("manufacturer")).toEqual(["Garmin"]); + }); + + it("omits query string when filter is empty", async () => { + let capturedURL = ""; + const mockFetch = async (url: string | URL | Request) => { + capturedURL = String(url); + return jsonResponse([]); + }; + + const client = new Client("http://localhost:8089", { + fetch: mockFetch as typeof fetch, + }); + await client.values({}); + expect(capturedURL).toBe("http://localhost:8089/values"); + }); + + it("omits query string when no filter", async () => { + let capturedURL = ""; + const mockFetch = async (url: string | URL | Request) => { + capturedURL = String(url); + return jsonResponse([]); + }; + + const client = new Client("http://localhost:8089", { + fetch: mockFetch as typeof fetch, + }); + await client.values(); + expect(capturedURL).toBe("http://localhost:8089/values"); + }); + it("throws HttpError on non-200", async () => { const mockFetch = async () => errorResponse(500, "internal error"); const client = new Client("http://localhost:8089", {