mushroomdb-client 0.1.2

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 ADDED
@@ -0,0 +1,281 @@
1
+ # mushroomdb-client
2
+
3
+ TypeScript client for the [mushroomdb](https://github.com/MatthewSherlin/mushroomdb) graph database.
4
+ Wraps the HTTP and WebSocket API exposed by `mushroomdb serve`.
5
+
6
+ > **Not yet published to npm.** Install from the repo path (see below).
7
+
8
+ ---
9
+
10
+ ## Installation (from repo)
11
+
12
+ ```sh
13
+ npm install /path/to/graph-db/clients/typescript
14
+ ```
15
+
16
+ Or add to `package.json`:
17
+
18
+ ```json
19
+ {
20
+ "dependencies": {
21
+ "mushroomdb-client": "file:../path/to/graph-db/clients/typescript"
22
+ }
23
+ }
24
+ ```
25
+
26
+ Node 18+ required. Fetch is built-in. For WebSocket in Node < 21, install `ws`:
27
+
28
+ ```sh
29
+ npm install ws
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ ```ts
37
+ import { MushroomClient } from 'mushroomdb-client';
38
+
39
+ const client = new MushroomClient('http://127.0.0.1:8080');
40
+
41
+ // Read query
42
+ const result = await client.query(
43
+ 'MATCH (p:Person) RETURN p.id AS id, p.name AS name LIMIT 10',
44
+ );
45
+ console.log(result.columns); // ['id', 'name']
46
+ console.log(result.rows); // [['person-01', 'Alice'], ...]
47
+
48
+ // Parameterised read
49
+ const row = await client.query(
50
+ 'MATCH (p:Person {id: $pid}) RETURN p.name AS name',
51
+ { params: { pid: 'person-01' } },
52
+ );
53
+
54
+ // Write (CREATE, MERGE, SET, DELETE)
55
+ await client.query("CREATE (n:Company {id: 'acme', name: 'Acme Corp'})");
56
+
57
+ // Bulk ingest
58
+ await client.ingest({
59
+ label: 'Product',
60
+ rows: [
61
+ { id: 'prod-1', name: 'Widget', price: 9.99 },
62
+ { id: 'prod-2', name: 'Gadget', price: 24.99 },
63
+ ],
64
+ });
65
+
66
+ // Stats
67
+ const stats = await client.stats();
68
+ console.log(stats.nodes_live, stats.edges);
69
+
70
+ // Rule suggestions
71
+ const report = await client.suggest();
72
+ console.log(report.truncated, report.suggestions.length);
73
+
74
+ // Graph algorithms
75
+ const pr = await client.algo('pagerank');
76
+ console.log(pr.scores.slice(0, 5)); // top-5 nodes by PageRank
77
+
78
+ const wcc = await client.algo('wcc');
79
+ console.log(wcc.components.length);
80
+
81
+ const deg = await client.algo('degree', { direction: 'out' });
82
+ console.log(deg.scores.slice(0, 5));
83
+ ```
84
+
85
+ ---
86
+
87
+ ## WebSocket subscriptions
88
+
89
+ Subscribe to post-commit events over `GET /subscribe`.
90
+
91
+ **No auto-reconnect in v1.** When the connection drops, no further events are
92
+ delivered. Reconnect manually if required.
93
+
94
+ **The `lagged` event** is passed to `onEvent` like any other event. It fires
95
+ when the server's per-subscriber queue (65,536 events) overflows. On receiving
96
+ it, re-read the affected graph state for lossless consumers.
97
+
98
+ ```ts
99
+ import WS from 'ws'; // Node < 21: npm install ws
100
+ import type { WsConstructor } from 'mushroomdb-client';
101
+
102
+ const handle = await client.subscribe(
103
+ {
104
+ rules: ['skill_fit'], // rule-fire events
105
+ writes: true, // node/prop write events
106
+ wsConstructor: WS as unknown as WsConstructor, // Node < 21 only
107
+ },
108
+ (ev) => {
109
+ switch (ev.type) {
110
+ case 'edge_fired':
111
+ console.log(`Rule ${ev.rule}: ${ev.src_key} → ${ev.dst_key} (seq ${ev.commit_seq})`);
112
+ break;
113
+ case 'node_inserted':
114
+ console.log(`New node: ${ev.key} (label ${ev.label})`);
115
+ break;
116
+ case 'lagged':
117
+ console.warn(`Missed ${ev.missed} events — re-read state`);
118
+ break;
119
+ }
120
+ },
121
+ );
122
+
123
+ // ... do work ...
124
+
125
+ // Always await close to avoid dangling handles.
126
+ await handle.close();
127
+ ```
128
+
129
+ ---
130
+
131
+ ## API reference
132
+
133
+ ### `new MushroomClient(baseUrl: string, opts?: { token?: string })`
134
+
135
+ Create a client. `baseUrl` is the HTTP base URL printed by `mushroomdb serve`,
136
+ e.g. `"http://127.0.0.1:8080"`. When `opts.token` is set, every HTTP fetch
137
+ sends `Authorization: Bearer <token>`.
138
+
139
+ ### `client.query(cypher, opts?) → Promise<QueryResult>`
140
+
141
+ Run a Cypher query (read or write). The server detects write statements and
142
+ acquires the write lock automatically. Returns `{ columns: string[], rows: CellValue[][] }`.
143
+
144
+ `opts.params` — bound parameters; values must be JSON scalars
145
+ (`string | number | boolean`).
146
+
147
+ ### `client.ingest(req) → Promise<IngestReport>`
148
+
149
+ Bulk-ingest nodes and optional edges. `req.label`, `req.rows` are required.
150
+ See `IngestOptions` and `IngestEdge` for advanced options.
151
+
152
+ ### `client.stats() → Promise<Stats>`
153
+
154
+ Returns live node/edge counts and per-rule statistics.
155
+
156
+ ### `client.suggest() → Promise<SuggestReport>`
157
+
158
+ Profile the database and return candidate linking rules. CPU-intensive;
159
+ capped at 5 s server-side. The `truncated` flag is `true` when the budget
160
+ fires early.
161
+
162
+ ### `client.explain(a, b) → Promise<Explanation[]>`
163
+
164
+ Wires `GET /explain?a=&b=`. Returns rule-derived edges between two node keys.
165
+
166
+ ### `client.createRule(def) → Promise<void>`
167
+
168
+ Wires `POST /rules` with a `RuleDef` JSON body.
169
+
170
+ ### `client.node(key) → Promise<NodeInfo | null>`
171
+
172
+ Wires `GET /node/{key}`. Returns `null` for an unknown key (HTTP 404).
173
+
174
+ ### `client.neighborhood(key, opts?) → Promise<Neighborhood>`
175
+
176
+ Wires `GET /node/{key}/neighborhood`. `opts.depth` defaults to the server's
177
+ (1). Result columns are `key`, `label`, `depth`.
178
+
179
+ ### `client.algo(name, config?) → Promise<AlgoReport>`
180
+
181
+ Run a graph algorithm:
182
+
183
+ | name | config type | result type |
184
+ |------|-------------|-------------|
185
+ | `"pagerank"` | `PageRankConfig` | `PageRankReport` — `{ scores: [key, score][], converged }` |
186
+ | `"wcc"` | `WccConfig` | `WccReport` — `{ components: [key, component_id][], truncated }` |
187
+ | `"degree"` | `DegreeConfig` | `DegreeReport` — `{ scores: [key, degree][], truncated }` |
188
+
189
+ All config fields are optional; server defaults apply.
190
+
191
+ ### `client.subscribe(opts, onEvent) → Promise<SubscribeHandle>`
192
+
193
+ Open a WebSocket subscription. Returns a handle with `close(): Promise<void>`.
194
+
195
+ ---
196
+
197
+ ## Error handling
198
+
199
+ All HTTP errors throw `MushroomError` with the server's error detail in
200
+ `err.detail` (and `err.message`):
201
+
202
+ ```ts
203
+ import { MushroomError } from 'mushroomdb-client';
204
+ try {
205
+ await client.query('BAD CYPHER');
206
+ } catch (err) {
207
+ if (err instanceof MushroomError) {
208
+ console.error('Query failed:', err.detail);
209
+ }
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Known server-side limitations
216
+
217
+ These are limitations of the mushroomdb server's Cypher implementation that
218
+ affect how you write queries through this client.
219
+
220
+ ### 1. `CREATE ... RETURN` is supported
221
+
222
+ You can include a `RETURN` clause directly after `CREATE` or `MERGE` to get
223
+ back the created or matched bindings in a single statement:
224
+
225
+ ```ts
226
+ // Single-statement create + return:
227
+ const result = await client.query(
228
+ "CREATE (n:Widget {id: 'w1', name: 'Sprocket'}) RETURN n.name AS nm"
229
+ );
230
+ // result.rows[0][0] === 'Sprocket'
231
+
232
+ // MERGE + RETURN (returns the node whether created or matched):
233
+ const r2 = await client.query(
234
+ "MERGE (n:Tag {id: 'rust'}) RETURN n"
235
+ );
236
+ ```
237
+
238
+ ### 2. Every node requires a string `id` property
239
+
240
+ When using `CREATE`, nodes must include an `id` field with a string value.
241
+ This is the key the server uses to identify the node:
242
+
243
+ ```ts
244
+ // WRONG — missing 'id'
245
+ await client.query("CREATE (n:Widget {name: 'Sprocket'})");
246
+
247
+ // CORRECT
248
+ await client.query("CREATE (n:Widget {id: 'w1', name: 'Sprocket'})");
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Node-only vs browser-compatible
254
+
255
+ | Feature | Browser | Node 18+ |
256
+ |---------|---------|----------|
257
+ | `query`, `ingest`, `stats`, `suggest`, `algo`, `explain`, `createRule`, `node`, `neighborhood` | Yes (uses `fetch`) | Yes |
258
+ | `subscribe` | Yes (uses global `WebSocket`) | Requires `wsConstructor` option + `ws` package |
259
+
260
+ ---
261
+
262
+ ## Running the tests
263
+
264
+ ```sh
265
+ cd clients/typescript
266
+ npm ci
267
+ npm test
268
+ ```
269
+
270
+ Tests spawn a real `mushroomdb` binary. The first run builds it with `cargo`
271
+ (takes ~30 s on a cold cache). If the build fails, all tests are **skipped**
272
+ with a clear message (not marked as failed).
273
+
274
+ Set `CARGO=/path/to/cargo` to use a specific cargo binary (required in CI or
275
+ on machines where cargo is not on `PATH`).
276
+
277
+ ---
278
+
279
+ ## License
280
+
281
+ Apache-2.0. Copyright 2026 Matthew Sherlin.
@@ -0,0 +1,176 @@
1
+ /**
2
+ * HTTP client for the mushroomdb server.
3
+ *
4
+ * Uses the browser-standard `fetch` API (built into Node 18+).
5
+ *
6
+ * ```ts
7
+ * import { MushroomClient } from 'mushroomdb-client';
8
+ *
9
+ * const client = new MushroomClient('http://127.0.0.1:8080');
10
+ * const result = await client.query('MATCH (n:Person) RETURN n.name LIMIT 10');
11
+ * console.log(result.columns, result.rows);
12
+ * ```
13
+ */
14
+ import { MushroomError, type DegreeConfig, type DegreeReport, type Explanation, type IngestReport, type IngestRequest, type Neighborhood, type NodeInfo, type PageRankConfig, type PageRankReport, type QueryResult, type RuleDef, type Stats, type SuggestReport, type WccConfig, type WccReport } from "./types.js";
15
+ import { type SubscribeHandle, type SubscribeOptions, type WsConstructor } from "./ws.js";
16
+ export type { AlgoDir, AlgoReport, CellValue, DegreeConfig, DegreeReport, Explanation, IngestEdge, IngestOptions, IngestReport, IngestRequest, Neighborhood, NodeInfo, PageRankConfig, PageRankReport, PredicateKind, PredicateSummary, QueryResult, RuleDef, RulePredicate, RuleStats, RuleSuggestion, Stats, SuggestReport, WccConfig, WccReport, } from "./types.js";
17
+ export { MushroomError };
18
+ /** Optional constructor flags for {@link MushroomClient}. */
19
+ export interface ClientOptions {
20
+ /**
21
+ * When set, sent as `Authorization: Bearer <token>` on every HTTP fetch.
22
+ * Cookie auth is a browser/explorer concern and is not implemented here.
23
+ */
24
+ token?: string;
25
+ }
26
+ /** Parameters for a Cypher query. Values must be JSON scalars. */
27
+ export type QueryParams = Record<string, string | number | boolean>;
28
+ /** Options accepted by {@link MushroomClient.query}. */
29
+ export interface QueryOptions {
30
+ /** Bound parameters. Values must be JSON scalars (string | number | boolean). */
31
+ params?: QueryParams;
32
+ }
33
+ /**
34
+ * HTTP + WebSocket client for mushroomdb.
35
+ *
36
+ * All methods use the browser-standard `fetch` API and are therefore
37
+ * compatible with both Node.js 18+ and modern browsers.
38
+ *
39
+ * **Node-only**: The `subscribe` method requires a WebSocket implementation.
40
+ * In Node < 21, install the `ws` package and pass `wsConstructor` in the
41
+ * subscribe options. See {@link SubscribeOptions}.
42
+ */
43
+ export declare class MushroomClient {
44
+ private readonly baseUrl;
45
+ private readonly wsBase;
46
+ private readonly token;
47
+ /**
48
+ * @param baseUrl HTTP base URL of the mushroomdb server, e.g.
49
+ * `"http://127.0.0.1:8080"`. Trailing slash is stripped.
50
+ * @param opts Optional `{ token }` — sent as `Authorization: Bearer`.
51
+ */
52
+ constructor(baseUrl: string, opts?: ClientOptions);
53
+ private url;
54
+ private wsUrl;
55
+ private authHeaders;
56
+ /**
57
+ * Execute a fetch and decode the JSON body.
58
+ * Throws {@link MushroomError} on non-2xx responses.
59
+ */
60
+ private fetchJson;
61
+ /**
62
+ * Run a Cypher query (read or write).
63
+ *
64
+ * The server auto-detects write statements (`CREATE`, `MERGE`, `SET`,
65
+ * `DELETE`) and acquires the appropriate lock. Both read and write queries
66
+ * go to `POST /query?format=json`.
67
+ *
68
+ * @param cypher Cypher query string.
69
+ * @param opts Optional bound parameters (JSON scalar values only).
70
+ * @returns Column names and a 2-D array of {@link CellValue} rows.
71
+ */
72
+ query(cypher: string, opts?: QueryOptions): Promise<QueryResult>;
73
+ /**
74
+ * Ingest nodes (and optional edges) into the database.
75
+ *
76
+ * Wraps `POST /ingest`. The server acquires the write lock, applies the
77
+ * rows to the WAL, and runs all rules incrementally.
78
+ *
79
+ * @param req Ingest payload — `label`, `rows`, optional `options` and `edges`.
80
+ * @returns Server ingest report (opaque; check for absence of errors).
81
+ */
82
+ ingest(req: IngestRequest): Promise<IngestReport>;
83
+ /**
84
+ * Get database-wide statistics.
85
+ *
86
+ * Wraps `GET /stats`. Returns live node/edge counts and per-rule stats.
87
+ */
88
+ stats(): Promise<Stats>;
89
+ /**
90
+ * Profile the database and return rule suggestions.
91
+ *
92
+ * Wraps `GET /suggest`. CPU-intensive — runs in the server's blocking
93
+ * thread-pool with a 5-second global budget. The {@link SuggestReport}
94
+ * includes a `truncated` flag when the budget fires early.
95
+ *
96
+ * Suggestions are not auto-applied; call `POST /rules` to create a rule.
97
+ */
98
+ suggest(): Promise<SuggestReport>;
99
+ /**
100
+ * Run a graph algorithm.
101
+ *
102
+ * Wraps `POST /algo/{pagerank|wcc|degree}`.
103
+ *
104
+ * @param algo Algorithm name.
105
+ * @param config Optional algorithm-specific configuration.
106
+ * @returns Algorithm report — see {@link PageRankReport}, {@link WccReport},
107
+ * {@link DegreeReport}.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const pr = await client.algo('pagerank') as PageRankReport;
112
+ * console.log(pr.scores.slice(0, 5));
113
+ * ```
114
+ */
115
+ algo(algo: "pagerank", config?: PageRankConfig): Promise<PageRankReport>;
116
+ algo(algo: "wcc", config?: WccConfig): Promise<WccReport>;
117
+ algo(algo: "degree", config?: DegreeConfig): Promise<DegreeReport>;
118
+ /**
119
+ * Explain rule-derived edges between two node keys.
120
+ *
121
+ * Wraps `GET /explain?a=&b=`.
122
+ */
123
+ explain(a: string, b: string): Promise<Explanation[]>;
124
+ /**
125
+ * Create a derivation rule.
126
+ *
127
+ * Wraps `POST /rules`. The server acquires the write lock, validates the
128
+ * {@link RuleDef}, and backfills matching pairs.
129
+ */
130
+ createRule(def: RuleDef): Promise<void>;
131
+ /**
132
+ * Fetch a node by key.
133
+ *
134
+ * Wraps `GET /node/{key}`. Returns `null` when the server answers 404
135
+ * (unknown key). Other HTTP errors throw {@link MushroomError}.
136
+ */
137
+ node(key: string): Promise<NodeInfo | null>;
138
+ /**
139
+ * Depth-N neighborhood of a node.
140
+ *
141
+ * Wraps `GET /node/{key}/neighborhood`. Default depth is the server's
142
+ * (1). Columns are `key`, `label`, `depth`.
143
+ */
144
+ neighborhood(key: string, opts?: {
145
+ depth?: number;
146
+ }): Promise<Neighborhood>;
147
+ /**
148
+ * Subscribe to post-commit events over WebSocket (`GET /subscribe`).
149
+ *
150
+ * Returns a promise that resolves when the server acknowledges the
151
+ * subscription (`{"subscribed":true}`). After that, `onEvent` is called
152
+ * for each {@link DbEvent}, including {@link DbEvent.lagged} frames.
153
+ *
154
+ * **No auto-reconnect in v1.** When the connection drops, no further events
155
+ * are delivered. Reconnect manually if required.
156
+ *
157
+ * **Always await `handle.close()`** when done — an open WebSocket keeps the
158
+ * Node.js event loop alive and will cause test hangs.
159
+ *
160
+ * **Node.js < 21**: pass `wsConstructor` — see {@link SubscribeOptions}.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import WS from 'ws';
165
+ * const handle = await client.subscribe(
166
+ * { writes: true, wsConstructor: WS as WsConstructor },
167
+ * (ev) => console.log(ev),
168
+ * );
169
+ * // ... do work ...
170
+ * await handle.close();
171
+ * ```
172
+ */
173
+ subscribe(opts: SubscribeOptions, onEvent: (event: import("./types.js").DbEvent) => void): Promise<SubscribeHandle>;
174
+ }
175
+ export type { SubscribeHandle, SubscribeOptions, WsConstructor };
176
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * HTTP client for the mushroomdb server.
3
+ *
4
+ * Uses the browser-standard `fetch` API (built into Node 18+).
5
+ *
6
+ * ```ts
7
+ * import { MushroomClient } from 'mushroomdb-client';
8
+ *
9
+ * const client = new MushroomClient('http://127.0.0.1:8080');
10
+ * const result = await client.query('MATCH (n:Person) RETURN n.name LIMIT 10');
11
+ * console.log(result.columns, result.rows);
12
+ * ```
13
+ */
14
+ import { MushroomError, } from "./types.js";
15
+ import { subscribe as wsSubscribe, } from "./ws.js";
16
+ export { MushroomError };
17
+ /**
18
+ * HTTP + WebSocket client for mushroomdb.
19
+ *
20
+ * All methods use the browser-standard `fetch` API and are therefore
21
+ * compatible with both Node.js 18+ and modern browsers.
22
+ *
23
+ * **Node-only**: The `subscribe` method requires a WebSocket implementation.
24
+ * In Node < 21, install the `ws` package and pass `wsConstructor` in the
25
+ * subscribe options. See {@link SubscribeOptions}.
26
+ */
27
+ export class MushroomClient {
28
+ baseUrl;
29
+ wsBase;
30
+ token;
31
+ /**
32
+ * @param baseUrl HTTP base URL of the mushroomdb server, e.g.
33
+ * `"http://127.0.0.1:8080"`. Trailing slash is stripped.
34
+ * @param opts Optional `{ token }` — sent as `Authorization: Bearer`.
35
+ */
36
+ constructor(baseUrl, opts) {
37
+ this.baseUrl = baseUrl.replace(/\/$/, "");
38
+ this.wsBase = this.baseUrl.replace(/^http/, "ws");
39
+ this.token = opts?.token;
40
+ }
41
+ // -------------------------------------------------------------------------
42
+ // Internal helpers
43
+ // -------------------------------------------------------------------------
44
+ url(path) {
45
+ return `${this.baseUrl}${path}`;
46
+ }
47
+ wsUrl(path) {
48
+ return `${this.wsBase}${path}`;
49
+ }
50
+ authHeaders() {
51
+ return this.token ? { Authorization: `Bearer ${this.token}` } : {};
52
+ }
53
+ /**
54
+ * Execute a fetch and decode the JSON body.
55
+ * Throws {@link MushroomError} on non-2xx responses.
56
+ */
57
+ async fetchJson(path, init) {
58
+ const resp = await fetch(this.url(path), {
59
+ ...init,
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ Accept: "application/json",
63
+ ...this.authHeaders(),
64
+ ...(init?.headers ?? {}),
65
+ },
66
+ });
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ const body = await resp.json();
69
+ if (!resp.ok) {
70
+ const detail = typeof body?.error === "string" ? body.error : `HTTP ${resp.status}`;
71
+ throw new MushroomError(detail);
72
+ }
73
+ return body;
74
+ }
75
+ // -------------------------------------------------------------------------
76
+ // HTTP endpoints
77
+ // -------------------------------------------------------------------------
78
+ /**
79
+ * Run a Cypher query (read or write).
80
+ *
81
+ * The server auto-detects write statements (`CREATE`, `MERGE`, `SET`,
82
+ * `DELETE`) and acquires the appropriate lock. Both read and write queries
83
+ * go to `POST /query?format=json`.
84
+ *
85
+ * @param cypher Cypher query string.
86
+ * @param opts Optional bound parameters (JSON scalar values only).
87
+ * @returns Column names and a 2-D array of {@link CellValue} rows.
88
+ */
89
+ async query(cypher, opts) {
90
+ return this.fetchJson("/query?format=json", {
91
+ method: "POST",
92
+ body: JSON.stringify({
93
+ cypher,
94
+ ...(opts?.params ? { params: opts.params } : {}),
95
+ }),
96
+ });
97
+ }
98
+ /**
99
+ * Ingest nodes (and optional edges) into the database.
100
+ *
101
+ * Wraps `POST /ingest`. The server acquires the write lock, applies the
102
+ * rows to the WAL, and runs all rules incrementally.
103
+ *
104
+ * @param req Ingest payload — `label`, `rows`, optional `options` and `edges`.
105
+ * @returns Server ingest report (opaque; check for absence of errors).
106
+ */
107
+ async ingest(req) {
108
+ return this.fetchJson("/ingest", {
109
+ method: "POST",
110
+ body: JSON.stringify(req),
111
+ });
112
+ }
113
+ /**
114
+ * Get database-wide statistics.
115
+ *
116
+ * Wraps `GET /stats`. Returns live node/edge counts and per-rule stats.
117
+ */
118
+ async stats() {
119
+ return this.fetchJson("/stats");
120
+ }
121
+ /**
122
+ * Profile the database and return rule suggestions.
123
+ *
124
+ * Wraps `GET /suggest`. CPU-intensive — runs in the server's blocking
125
+ * thread-pool with a 5-second global budget. The {@link SuggestReport}
126
+ * includes a `truncated` flag when the budget fires early.
127
+ *
128
+ * Suggestions are not auto-applied; call `POST /rules` to create a rule.
129
+ */
130
+ async suggest() {
131
+ return this.fetchJson("/suggest");
132
+ }
133
+ async algo(algo, config) {
134
+ // The server structs carry #[serde(default)], so sending only the fields
135
+ // the caller explicitly set (or an empty body {}) is valid — the server
136
+ // fills in its own defaults for any missing fields.
137
+ return this.fetchJson(`/algo/${algo}`, {
138
+ method: "POST",
139
+ body: JSON.stringify(config ?? {}),
140
+ });
141
+ }
142
+ /**
143
+ * Explain rule-derived edges between two node keys.
144
+ *
145
+ * Wraps `GET /explain?a=&b=`.
146
+ */
147
+ async explain(a, b) {
148
+ const qs = new URLSearchParams({ a, b });
149
+ return this.fetchJson(`/explain?${qs.toString()}`);
150
+ }
151
+ /**
152
+ * Create a derivation rule.
153
+ *
154
+ * Wraps `POST /rules`. The server acquires the write lock, validates the
155
+ * {@link RuleDef}, and backfills matching pairs.
156
+ */
157
+ async createRule(def) {
158
+ await this.fetchJson("/rules", {
159
+ method: "POST",
160
+ body: JSON.stringify(def),
161
+ });
162
+ }
163
+ /**
164
+ * Fetch a node by key.
165
+ *
166
+ * Wraps `GET /node/{key}`. Returns `null` when the server answers 404
167
+ * (unknown key). Other HTTP errors throw {@link MushroomError}.
168
+ */
169
+ async node(key) {
170
+ const resp = await fetch(this.url(`/node/${encodeURIComponent(key)}`), {
171
+ headers: {
172
+ Accept: "application/json",
173
+ ...this.authHeaders(),
174
+ },
175
+ });
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ const body = await resp.json();
178
+ if (resp.status === 404) {
179
+ return null;
180
+ }
181
+ if (!resp.ok) {
182
+ const detail = typeof body?.error === "string" ? body.error : `HTTP ${resp.status}`;
183
+ throw new MushroomError(detail);
184
+ }
185
+ return body;
186
+ }
187
+ /**
188
+ * Depth-N neighborhood of a node.
189
+ *
190
+ * Wraps `GET /node/{key}/neighborhood`. Default depth is the server's
191
+ * (1). Columns are `key`, `label`, `depth`.
192
+ */
193
+ async neighborhood(key, opts) {
194
+ const qs = new URLSearchParams();
195
+ if (opts?.depth !== undefined) {
196
+ qs.set("depth", String(opts.depth));
197
+ }
198
+ const query = qs.toString();
199
+ const path = `/node/${encodeURIComponent(key)}/neighborhood${query ? `?${query}` : ""}`;
200
+ return this.fetchJson(path);
201
+ }
202
+ // -------------------------------------------------------------------------
203
+ // WebSocket
204
+ // -------------------------------------------------------------------------
205
+ /**
206
+ * Subscribe to post-commit events over WebSocket (`GET /subscribe`).
207
+ *
208
+ * Returns a promise that resolves when the server acknowledges the
209
+ * subscription (`{"subscribed":true}`). After that, `onEvent` is called
210
+ * for each {@link DbEvent}, including {@link DbEvent.lagged} frames.
211
+ *
212
+ * **No auto-reconnect in v1.** When the connection drops, no further events
213
+ * are delivered. Reconnect manually if required.
214
+ *
215
+ * **Always await `handle.close()`** when done — an open WebSocket keeps the
216
+ * Node.js event loop alive and will cause test hangs.
217
+ *
218
+ * **Node.js < 21**: pass `wsConstructor` — see {@link SubscribeOptions}.
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * import WS from 'ws';
223
+ * const handle = await client.subscribe(
224
+ * { writes: true, wsConstructor: WS as WsConstructor },
225
+ * (ev) => console.log(ev),
226
+ * );
227
+ * // ... do work ...
228
+ * await handle.close();
229
+ * ```
230
+ */
231
+ subscribe(opts, onEvent) {
232
+ let url = this.wsUrl("/subscribe");
233
+ if (this.token) {
234
+ url += `?token=${encodeURIComponent(this.token)}`;
235
+ }
236
+ return wsSubscribe(url, opts, onEvent);
237
+ }
238
+ }
239
+ //# sourceMappingURL=client.js.map