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 +281 -0
- package/dist/client.d.ts +176 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +239 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +406 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/dist/ws.d.ts +92 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +139 -0
- package/dist/ws.js.map +1 -0
- package/package.json +42 -0
- package/src/client.ts +364 -0
- package/src/index.ts +47 -0
- package/src/types.ts +460 -0
- package/src/ws.ts +200 -0
package/dist/ws.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket subscription over `GET /subscribe`.
|
|
3
|
+
*
|
|
4
|
+
* # Protocol (matches crates/server/src/subscribe.rs)
|
|
5
|
+
*
|
|
6
|
+
* 1. Connect to `ws[s]://<host>/subscribe`.
|
|
7
|
+
* 2. Server waits for one JSON subscribe message: `{rules?, writes?}`.
|
|
8
|
+
* 3. Server responds with `{"subscribed":true}`.
|
|
9
|
+
* 4. Server streams DbEvent JSON frames until the connection closes.
|
|
10
|
+
*
|
|
11
|
+
* # Reconnection
|
|
12
|
+
*
|
|
13
|
+
* Auto-reconnect is NOT implemented in v1. When the connection drops
|
|
14
|
+
* (network error, server restart), no further events are delivered. The
|
|
15
|
+
* caller is responsible for reconnecting if required.
|
|
16
|
+
*
|
|
17
|
+
* # Lagged events
|
|
18
|
+
*
|
|
19
|
+
* If the server's per-subscriber queue overflows, it emits a
|
|
20
|
+
* `{"type":"lagged","missed":N}` frame. This is passed to `onEvent` like any
|
|
21
|
+
* other event. For lossless consumers: on receiving a `lagged` event, re-read
|
|
22
|
+
* the affected graph state via a query.
|
|
23
|
+
*
|
|
24
|
+
* # Node.js usage
|
|
25
|
+
*
|
|
26
|
+
* The browser WebSocket global is not present in Node < 21. Pass the `ws`
|
|
27
|
+
* package's WebSocket class via `opts.wsConstructor`:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* import WS from 'ws';
|
|
31
|
+
* const handle = await subscribe(wsUrl, { writes: true, wsConstructor: WS as WsConstructor }, onEvent);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { MushroomError } from "./types.js";
|
|
35
|
+
/** Coerce an unknown message `data` value to a UTF-8 string. */
|
|
36
|
+
function dataToString(data) {
|
|
37
|
+
if (typeof data === "string")
|
|
38
|
+
return data;
|
|
39
|
+
// Node.js ws package delivers Buffer objects for text frames.
|
|
40
|
+
if (data != null && typeof data.toString === "function") {
|
|
41
|
+
return data.toString();
|
|
42
|
+
}
|
|
43
|
+
return String(data);
|
|
44
|
+
}
|
|
45
|
+
/** Resolve the WebSocket constructor: explicit option → global. */
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
function resolveWsConstructor(opt) {
|
|
48
|
+
if (opt)
|
|
49
|
+
return opt;
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
const g = globalThis;
|
|
52
|
+
if (typeof g["WebSocket"] === "function")
|
|
53
|
+
return g["WebSocket"];
|
|
54
|
+
throw new Error("No WebSocket implementation available. " +
|
|
55
|
+
"In Node.js < 21, install the `ws` package and pass " +
|
|
56
|
+
"`wsConstructor: WS as WsConstructor` in the options.");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Open a `GET /subscribe` WebSocket and begin streaming {@link DbEvent}s.
|
|
60
|
+
*
|
|
61
|
+
* Resolves when the server acknowledges the subscribe message
|
|
62
|
+
* (`{"subscribed":true}`). Rejects on connection failure or if the server
|
|
63
|
+
* returns an error (e.g. unknown rule name).
|
|
64
|
+
*
|
|
65
|
+
* @param wsUrl Full WebSocket URL, e.g. `ws://127.0.0.1:8080/subscribe`.
|
|
66
|
+
* @param opts Subscribe options — rules, writes flag, optional wsConstructor.
|
|
67
|
+
* @param onEvent Callback invoked for each {@link DbEvent}, including `lagged`.
|
|
68
|
+
*/
|
|
69
|
+
export async function subscribe(wsUrl, opts, onEvent) {
|
|
70
|
+
const WS = resolveWsConstructor(opts.wsConstructor);
|
|
71
|
+
const ws = new WS(wsUrl);
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let subscribed = false;
|
|
74
|
+
let closeResolve = null;
|
|
75
|
+
const closePromise = new Promise((res) => {
|
|
76
|
+
closeResolve = res;
|
|
77
|
+
});
|
|
78
|
+
ws.onopen = () => {
|
|
79
|
+
const msg = {
|
|
80
|
+
rules: opts.rules ?? [],
|
|
81
|
+
writes: opts.writes ?? false,
|
|
82
|
+
};
|
|
83
|
+
ws.send(JSON.stringify(msg));
|
|
84
|
+
};
|
|
85
|
+
ws.onmessage = (ev) => {
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = dataToString(ev.data);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return; // unreadable frame — skip
|
|
92
|
+
}
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(text);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return; // unparseable frame — skip
|
|
99
|
+
}
|
|
100
|
+
const frame = parsed;
|
|
101
|
+
if (!subscribed) {
|
|
102
|
+
if (frame["subscribed"] === true) {
|
|
103
|
+
subscribed = true;
|
|
104
|
+
resolve({
|
|
105
|
+
close() {
|
|
106
|
+
ws.close();
|
|
107
|
+
return closePromise;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else if (typeof frame["error"] === "string") {
|
|
112
|
+
reject(new MushroomError(frame["error"]));
|
|
113
|
+
ws.close();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
reject(new Error("Unexpected subscribe response: " + text));
|
|
117
|
+
ws.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
onEvent(frame);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
ws.onerror = (err) => {
|
|
125
|
+
if (!subscribed) {
|
|
126
|
+
reject(err instanceof Error
|
|
127
|
+
? err
|
|
128
|
+
: new Error("WebSocket error before subscribe ack"));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
ws.onclose = () => {
|
|
132
|
+
if (!subscribed) {
|
|
133
|
+
reject(new Error("WebSocket closed before subscribe ack"));
|
|
134
|
+
}
|
|
135
|
+
closeResolve?.();
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=ws.js.map
|
package/dist/ws.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mushroomdb-client",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "TypeScript client for the mushroomdb graph database HTTP + WebSocket API",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/MatthewSherlin/mushroomdb"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"@types/ws": "^8.5.12",
|
|
35
|
+
"typescript": "^7.0.2",
|
|
36
|
+
"vitest": "^4.1.10",
|
|
37
|
+
"ws": "^8.18.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
|
|
15
|
+
import {
|
|
16
|
+
MushroomError,
|
|
17
|
+
type AlgoReport,
|
|
18
|
+
type DegreeConfig,
|
|
19
|
+
type DegreeReport,
|
|
20
|
+
type Explanation,
|
|
21
|
+
type IngestReport,
|
|
22
|
+
type IngestRequest,
|
|
23
|
+
type Neighborhood,
|
|
24
|
+
type NodeInfo,
|
|
25
|
+
type PageRankConfig,
|
|
26
|
+
type PageRankReport,
|
|
27
|
+
type QueryResult,
|
|
28
|
+
type RuleDef,
|
|
29
|
+
type Stats,
|
|
30
|
+
type SuggestReport,
|
|
31
|
+
type WccConfig,
|
|
32
|
+
type WccReport,
|
|
33
|
+
} from "./types.js";
|
|
34
|
+
import {
|
|
35
|
+
subscribe as wsSubscribe,
|
|
36
|
+
type SubscribeHandle,
|
|
37
|
+
type SubscribeOptions,
|
|
38
|
+
type WsConstructor,
|
|
39
|
+
} from "./ws.js";
|
|
40
|
+
|
|
41
|
+
export type {
|
|
42
|
+
AlgoDir,
|
|
43
|
+
AlgoReport,
|
|
44
|
+
CellValue,
|
|
45
|
+
DegreeConfig,
|
|
46
|
+
DegreeReport,
|
|
47
|
+
Explanation,
|
|
48
|
+
IngestEdge,
|
|
49
|
+
IngestOptions,
|
|
50
|
+
IngestReport,
|
|
51
|
+
IngestRequest,
|
|
52
|
+
Neighborhood,
|
|
53
|
+
NodeInfo,
|
|
54
|
+
PageRankConfig,
|
|
55
|
+
PageRankReport,
|
|
56
|
+
PredicateKind,
|
|
57
|
+
PredicateSummary,
|
|
58
|
+
QueryResult,
|
|
59
|
+
RuleDef,
|
|
60
|
+
RulePredicate,
|
|
61
|
+
RuleStats,
|
|
62
|
+
RuleSuggestion,
|
|
63
|
+
Stats,
|
|
64
|
+
SuggestReport,
|
|
65
|
+
WccConfig,
|
|
66
|
+
WccReport,
|
|
67
|
+
} from "./types.js";
|
|
68
|
+
export { MushroomError };
|
|
69
|
+
|
|
70
|
+
/** Optional constructor flags for {@link MushroomClient}. */
|
|
71
|
+
export interface ClientOptions {
|
|
72
|
+
/**
|
|
73
|
+
* When set, sent as `Authorization: Bearer <token>` on every HTTP fetch.
|
|
74
|
+
* Cookie auth is a browser/explorer concern and is not implemented here.
|
|
75
|
+
*/
|
|
76
|
+
token?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Parameters for a Cypher query. Values must be JSON scalars. */
|
|
80
|
+
export type QueryParams = Record<string, string | number | boolean>;
|
|
81
|
+
|
|
82
|
+
/** Options accepted by {@link MushroomClient.query}. */
|
|
83
|
+
export interface QueryOptions {
|
|
84
|
+
/** Bound parameters. Values must be JSON scalars (string | number | boolean). */
|
|
85
|
+
params?: QueryParams;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* HTTP + WebSocket client for mushroomdb.
|
|
90
|
+
*
|
|
91
|
+
* All methods use the browser-standard `fetch` API and are therefore
|
|
92
|
+
* compatible with both Node.js 18+ and modern browsers.
|
|
93
|
+
*
|
|
94
|
+
* **Node-only**: The `subscribe` method requires a WebSocket implementation.
|
|
95
|
+
* In Node < 21, install the `ws` package and pass `wsConstructor` in the
|
|
96
|
+
* subscribe options. See {@link SubscribeOptions}.
|
|
97
|
+
*/
|
|
98
|
+
export class MushroomClient {
|
|
99
|
+
private readonly baseUrl: string;
|
|
100
|
+
private readonly wsBase: string;
|
|
101
|
+
private readonly token: string | undefined;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param baseUrl HTTP base URL of the mushroomdb server, e.g.
|
|
105
|
+
* `"http://127.0.0.1:8080"`. Trailing slash is stripped.
|
|
106
|
+
* @param opts Optional `{ token }` — sent as `Authorization: Bearer`.
|
|
107
|
+
*/
|
|
108
|
+
constructor(baseUrl: string, opts?: ClientOptions) {
|
|
109
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
110
|
+
this.wsBase = this.baseUrl.replace(/^http/, "ws");
|
|
111
|
+
this.token = opts?.token;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// Internal helpers
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
private url(path: string): string {
|
|
119
|
+
return `${this.baseUrl}${path}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private wsUrl(path: string): string {
|
|
123
|
+
return `${this.wsBase}${path}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private authHeaders(): Record<string, string> {
|
|
127
|
+
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute a fetch and decode the JSON body.
|
|
132
|
+
* Throws {@link MushroomError} on non-2xx responses.
|
|
133
|
+
*/
|
|
134
|
+
private async fetchJson<T>(
|
|
135
|
+
path: string,
|
|
136
|
+
init?: RequestInit,
|
|
137
|
+
): Promise<T> {
|
|
138
|
+
const resp = await fetch(this.url(path), {
|
|
139
|
+
...init,
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
...this.authHeaders(),
|
|
144
|
+
...(init?.headers ?? {}),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
const body: any = await resp.json();
|
|
149
|
+
if (!resp.ok) {
|
|
150
|
+
const detail: string =
|
|
151
|
+
typeof body?.error === "string" ? body.error : `HTTP ${resp.status}`;
|
|
152
|
+
throw new MushroomError(detail);
|
|
153
|
+
}
|
|
154
|
+
return body as T;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
// HTTP endpoints
|
|
159
|
+
// -------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run a Cypher query (read or write).
|
|
163
|
+
*
|
|
164
|
+
* The server auto-detects write statements (`CREATE`, `MERGE`, `SET`,
|
|
165
|
+
* `DELETE`) and acquires the appropriate lock. Both read and write queries
|
|
166
|
+
* go to `POST /query?format=json`.
|
|
167
|
+
*
|
|
168
|
+
* @param cypher Cypher query string.
|
|
169
|
+
* @param opts Optional bound parameters (JSON scalar values only).
|
|
170
|
+
* @returns Column names and a 2-D array of {@link CellValue} rows.
|
|
171
|
+
*/
|
|
172
|
+
async query(cypher: string, opts?: QueryOptions): Promise<QueryResult> {
|
|
173
|
+
return this.fetchJson<QueryResult>("/query?format=json", {
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
cypher,
|
|
177
|
+
...(opts?.params ? { params: opts.params } : {}),
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Ingest nodes (and optional edges) into the database.
|
|
184
|
+
*
|
|
185
|
+
* Wraps `POST /ingest`. The server acquires the write lock, applies the
|
|
186
|
+
* rows to the WAL, and runs all rules incrementally.
|
|
187
|
+
*
|
|
188
|
+
* @param req Ingest payload — `label`, `rows`, optional `options` and `edges`.
|
|
189
|
+
* @returns Server ingest report (opaque; check for absence of errors).
|
|
190
|
+
*/
|
|
191
|
+
async ingest(req: IngestRequest): Promise<IngestReport> {
|
|
192
|
+
return this.fetchJson<IngestReport>("/ingest", {
|
|
193
|
+
method: "POST",
|
|
194
|
+
body: JSON.stringify(req),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get database-wide statistics.
|
|
200
|
+
*
|
|
201
|
+
* Wraps `GET /stats`. Returns live node/edge counts and per-rule stats.
|
|
202
|
+
*/
|
|
203
|
+
async stats(): Promise<Stats> {
|
|
204
|
+
return this.fetchJson<Stats>("/stats");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Profile the database and return rule suggestions.
|
|
209
|
+
*
|
|
210
|
+
* Wraps `GET /suggest`. CPU-intensive — runs in the server's blocking
|
|
211
|
+
* thread-pool with a 5-second global budget. The {@link SuggestReport}
|
|
212
|
+
* includes a `truncated` flag when the budget fires early.
|
|
213
|
+
*
|
|
214
|
+
* Suggestions are not auto-applied; call `POST /rules` to create a rule.
|
|
215
|
+
*/
|
|
216
|
+
async suggest(): Promise<SuggestReport> {
|
|
217
|
+
return this.fetchJson<SuggestReport>("/suggest");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Run a graph algorithm.
|
|
222
|
+
*
|
|
223
|
+
* Wraps `POST /algo/{pagerank|wcc|degree}`.
|
|
224
|
+
*
|
|
225
|
+
* @param algo Algorithm name.
|
|
226
|
+
* @param config Optional algorithm-specific configuration.
|
|
227
|
+
* @returns Algorithm report — see {@link PageRankReport}, {@link WccReport},
|
|
228
|
+
* {@link DegreeReport}.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* const pr = await client.algo('pagerank') as PageRankReport;
|
|
233
|
+
* console.log(pr.scores.slice(0, 5));
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
async algo(algo: "pagerank", config?: PageRankConfig): Promise<PageRankReport>;
|
|
237
|
+
async algo(algo: "wcc", config?: WccConfig): Promise<WccReport>;
|
|
238
|
+
async algo(algo: "degree", config?: DegreeConfig): Promise<DegreeReport>;
|
|
239
|
+
async algo(
|
|
240
|
+
algo: "pagerank" | "wcc" | "degree",
|
|
241
|
+
config?: PageRankConfig | WccConfig | DegreeConfig,
|
|
242
|
+
): Promise<AlgoReport> {
|
|
243
|
+
// The server structs carry #[serde(default)], so sending only the fields
|
|
244
|
+
// the caller explicitly set (or an empty body {}) is valid — the server
|
|
245
|
+
// fills in its own defaults for any missing fields.
|
|
246
|
+
return this.fetchJson<AlgoReport>(`/algo/${algo}`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body: JSON.stringify(config ?? {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Explain rule-derived edges between two node keys.
|
|
254
|
+
*
|
|
255
|
+
* Wraps `GET /explain?a=&b=`.
|
|
256
|
+
*/
|
|
257
|
+
async explain(a: string, b: string): Promise<Explanation[]> {
|
|
258
|
+
const qs = new URLSearchParams({ a, b });
|
|
259
|
+
return this.fetchJson<Explanation[]>(`/explain?${qs.toString()}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create a derivation rule.
|
|
264
|
+
*
|
|
265
|
+
* Wraps `POST /rules`. The server acquires the write lock, validates the
|
|
266
|
+
* {@link RuleDef}, and backfills matching pairs.
|
|
267
|
+
*/
|
|
268
|
+
async createRule(def: RuleDef): Promise<void> {
|
|
269
|
+
await this.fetchJson("/rules", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
body: JSON.stringify(def),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Fetch a node by key.
|
|
277
|
+
*
|
|
278
|
+
* Wraps `GET /node/{key}`. Returns `null` when the server answers 404
|
|
279
|
+
* (unknown key). Other HTTP errors throw {@link MushroomError}.
|
|
280
|
+
*/
|
|
281
|
+
async node(key: string): Promise<NodeInfo | null> {
|
|
282
|
+
const resp = await fetch(this.url(`/node/${encodeURIComponent(key)}`), {
|
|
283
|
+
headers: {
|
|
284
|
+
Accept: "application/json",
|
|
285
|
+
...this.authHeaders(),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
+
const body: any = await resp.json();
|
|
290
|
+
if (resp.status === 404) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (!resp.ok) {
|
|
294
|
+
const detail: string =
|
|
295
|
+
typeof body?.error === "string" ? body.error : `HTTP ${resp.status}`;
|
|
296
|
+
throw new MushroomError(detail);
|
|
297
|
+
}
|
|
298
|
+
return body as NodeInfo;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Depth-N neighborhood of a node.
|
|
303
|
+
*
|
|
304
|
+
* Wraps `GET /node/{key}/neighborhood`. Default depth is the server's
|
|
305
|
+
* (1). Columns are `key`, `label`, `depth`.
|
|
306
|
+
*/
|
|
307
|
+
async neighborhood(
|
|
308
|
+
key: string,
|
|
309
|
+
opts?: { depth?: number },
|
|
310
|
+
): Promise<Neighborhood> {
|
|
311
|
+
const qs = new URLSearchParams();
|
|
312
|
+
if (opts?.depth !== undefined) {
|
|
313
|
+
qs.set("depth", String(opts.depth));
|
|
314
|
+
}
|
|
315
|
+
const query = qs.toString();
|
|
316
|
+
const path = `/node/${encodeURIComponent(key)}/neighborhood${
|
|
317
|
+
query ? `?${query}` : ""
|
|
318
|
+
}`;
|
|
319
|
+
return this.fetchJson<Neighborhood>(path);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// -------------------------------------------------------------------------
|
|
323
|
+
// WebSocket
|
|
324
|
+
// -------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Subscribe to post-commit events over WebSocket (`GET /subscribe`).
|
|
328
|
+
*
|
|
329
|
+
* Returns a promise that resolves when the server acknowledges the
|
|
330
|
+
* subscription (`{"subscribed":true}`). After that, `onEvent` is called
|
|
331
|
+
* for each {@link DbEvent}, including {@link DbEvent.lagged} frames.
|
|
332
|
+
*
|
|
333
|
+
* **No auto-reconnect in v1.** When the connection drops, no further events
|
|
334
|
+
* are delivered. Reconnect manually if required.
|
|
335
|
+
*
|
|
336
|
+
* **Always await `handle.close()`** when done — an open WebSocket keeps the
|
|
337
|
+
* Node.js event loop alive and will cause test hangs.
|
|
338
|
+
*
|
|
339
|
+
* **Node.js < 21**: pass `wsConstructor` — see {@link SubscribeOptions}.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```ts
|
|
343
|
+
* import WS from 'ws';
|
|
344
|
+
* const handle = await client.subscribe(
|
|
345
|
+
* { writes: true, wsConstructor: WS as WsConstructor },
|
|
346
|
+
* (ev) => console.log(ev),
|
|
347
|
+
* );
|
|
348
|
+
* // ... do work ...
|
|
349
|
+
* await handle.close();
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
subscribe(
|
|
353
|
+
opts: SubscribeOptions,
|
|
354
|
+
onEvent: (event: import("./types.js").DbEvent) => void,
|
|
355
|
+
): Promise<SubscribeHandle> {
|
|
356
|
+
let url = this.wsUrl("/subscribe");
|
|
357
|
+
if (this.token) {
|
|
358
|
+
url += `?token=${encodeURIComponent(this.token)}`;
|
|
359
|
+
}
|
|
360
|
+
return wsSubscribe(url, opts, onEvent);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export type { SubscribeHandle, SubscribeOptions, WsConstructor };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mushroomdb-client — TypeScript client for mushroomdb.
|
|
3
|
+
*
|
|
4
|
+
* Entry point re-exports everything callers need.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { MushroomClient, MushroomError } from "./client.js";
|
|
8
|
+
export type {
|
|
9
|
+
AlgoDir,
|
|
10
|
+
AlgoReport,
|
|
11
|
+
CellValue,
|
|
12
|
+
ClientOptions,
|
|
13
|
+
DegreeConfig,
|
|
14
|
+
DegreeReport,
|
|
15
|
+
Explanation,
|
|
16
|
+
IngestEdge,
|
|
17
|
+
IngestOptions,
|
|
18
|
+
IngestReport,
|
|
19
|
+
IngestRequest,
|
|
20
|
+
Neighborhood,
|
|
21
|
+
NodeInfo,
|
|
22
|
+
PageRankConfig,
|
|
23
|
+
PageRankReport,
|
|
24
|
+
PredicateKind,
|
|
25
|
+
PredicateSummary,
|
|
26
|
+
QueryOptions,
|
|
27
|
+
QueryParams,
|
|
28
|
+
QueryResult,
|
|
29
|
+
RuleDef,
|
|
30
|
+
RulePredicate,
|
|
31
|
+
RuleStats,
|
|
32
|
+
RuleSuggestion,
|
|
33
|
+
Stats,
|
|
34
|
+
SuggestReport,
|
|
35
|
+
WccConfig,
|
|
36
|
+
WccReport,
|
|
37
|
+
} from "./client.js";
|
|
38
|
+
export type {
|
|
39
|
+
DbEvent,
|
|
40
|
+
SubscribeMessage,
|
|
41
|
+
} from "./types.js";
|
|
42
|
+
export type {
|
|
43
|
+
SubscribeHandle,
|
|
44
|
+
SubscribeOptions,
|
|
45
|
+
WsConstructor,
|
|
46
|
+
WsLike,
|
|
47
|
+
} from "./ws.js";
|