@grafema/rfdb-client 0.1.0-alpha.1

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/client.js ADDED
@@ -0,0 +1,429 @@
1
+ /**
2
+ * RFDBClient - Unix socket client for RFDB server
3
+ *
4
+ * Provides the same API as GraphEngine NAPI binding but communicates
5
+ * with a separate rfdb-server process over Unix socket + MessagePack.
6
+ */
7
+ import { createConnection } from 'net';
8
+ import { encode, decode } from '@msgpack/msgpack';
9
+ import { EventEmitter } from 'events';
10
+ export class RFDBClient extends EventEmitter {
11
+ socketPath;
12
+ socket;
13
+ connected;
14
+ pending;
15
+ reqId;
16
+ buffer;
17
+ constructor(socketPath = '/tmp/rfdb.sock') {
18
+ super();
19
+ this.socketPath = socketPath;
20
+ this.socket = null;
21
+ this.connected = false;
22
+ this.pending = new Map();
23
+ this.reqId = 0;
24
+ this.buffer = Buffer.alloc(0);
25
+ }
26
+ /**
27
+ * Connect to RFDB server
28
+ */
29
+ async connect() {
30
+ if (this.connected)
31
+ return;
32
+ return new Promise((resolve, reject) => {
33
+ this.socket = createConnection(this.socketPath);
34
+ this.socket.on('connect', () => {
35
+ this.connected = true;
36
+ this.emit('connected');
37
+ resolve();
38
+ });
39
+ this.socket.on('error', (err) => {
40
+ if (!this.connected) {
41
+ reject(err);
42
+ }
43
+ else {
44
+ this.emit('error', err);
45
+ }
46
+ });
47
+ this.socket.on('close', () => {
48
+ this.connected = false;
49
+ this.emit('disconnected');
50
+ // Reject all pending requests
51
+ for (const [, { reject }] of this.pending) {
52
+ reject(new Error('Connection closed'));
53
+ }
54
+ this.pending.clear();
55
+ });
56
+ this.socket.on('data', (chunk) => {
57
+ this._handleData(chunk);
58
+ });
59
+ });
60
+ }
61
+ /**
62
+ * Handle incoming data, parse framed messages
63
+ */
64
+ _handleData(chunk) {
65
+ this.buffer = Buffer.concat([this.buffer, chunk]);
66
+ while (this.buffer.length >= 4) {
67
+ // Read length prefix (4 bytes, big-endian)
68
+ const msgLen = this.buffer.readUInt32BE(0);
69
+ if (this.buffer.length < 4 + msgLen) {
70
+ // Not enough data yet
71
+ break;
72
+ }
73
+ // Extract message
74
+ const msgBytes = this.buffer.subarray(4, 4 + msgLen);
75
+ this.buffer = this.buffer.subarray(4 + msgLen);
76
+ // Decode and dispatch
77
+ try {
78
+ const response = decode(msgBytes);
79
+ this._handleResponse(response);
80
+ }
81
+ catch (err) {
82
+ this.emit('error', new Error(`Failed to decode response: ${err.message}`));
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Handle decoded response
88
+ */
89
+ _handleResponse(response) {
90
+ if (this.pending.size === 0) {
91
+ this.emit('error', new Error('Received response with no pending request'));
92
+ return;
93
+ }
94
+ // Get the oldest pending request (FIFO)
95
+ const [id, { resolve, reject }] = this.pending.entries().next().value;
96
+ this.pending.delete(id);
97
+ if (response.error) {
98
+ reject(new Error(response.error));
99
+ }
100
+ else {
101
+ resolve(response);
102
+ }
103
+ }
104
+ /**
105
+ * Send a request and wait for response
106
+ */
107
+ async _send(cmd, payload = {}) {
108
+ if (!this.connected || !this.socket) {
109
+ throw new Error('Not connected to RFDB server');
110
+ }
111
+ const request = { cmd, ...payload };
112
+ const msgBytes = encode(request);
113
+ return new Promise((resolve, reject) => {
114
+ const id = this.reqId++;
115
+ this.pending.set(id, { resolve, reject });
116
+ // Write length prefix + message
117
+ const header = Buffer.alloc(4);
118
+ header.writeUInt32BE(msgBytes.length);
119
+ this.socket.write(Buffer.concat([header, Buffer.from(msgBytes)]));
120
+ });
121
+ }
122
+ // ===========================================================================
123
+ // Write Operations
124
+ // ===========================================================================
125
+ /**
126
+ * Add nodes to the graph
127
+ */
128
+ async addNodes(nodes) {
129
+ const wireNodes = nodes.map(n => ({
130
+ id: String(n.id),
131
+ nodeType: (n.node_type || n.nodeType || n.type || 'UNKNOWN'),
132
+ name: n.name || '',
133
+ file: n.file || '',
134
+ exported: n.exported || false,
135
+ metadata: typeof n.metadata === 'string' ? n.metadata : JSON.stringify(n.metadata || {}),
136
+ }));
137
+ return this._send('addNodes', { nodes: wireNodes });
138
+ }
139
+ /**
140
+ * Add edges to the graph
141
+ */
142
+ async addEdges(edges, skipValidation = false) {
143
+ const wireEdges = edges.map(e => ({
144
+ src: String(e.src),
145
+ dst: String(e.dst),
146
+ edgeType: (e.edge_type || e.edgeType || e.type || 'UNKNOWN'),
147
+ metadata: typeof e.metadata === 'string' ? e.metadata : JSON.stringify(e.metadata || {}),
148
+ }));
149
+ return this._send('addEdges', { edges: wireEdges, skipValidation });
150
+ }
151
+ /**
152
+ * Delete a node
153
+ */
154
+ async deleteNode(id) {
155
+ return this._send('deleteNode', { id: String(id) });
156
+ }
157
+ /**
158
+ * Delete an edge
159
+ */
160
+ async deleteEdge(src, dst, edgeType) {
161
+ return this._send('deleteEdge', {
162
+ src: String(src),
163
+ dst: String(dst),
164
+ edgeType
165
+ });
166
+ }
167
+ // ===========================================================================
168
+ // Read Operations
169
+ // ===========================================================================
170
+ /**
171
+ * Get a node by ID
172
+ */
173
+ async getNode(id) {
174
+ const response = await this._send('getNode', { id: String(id) });
175
+ return response.node || null;
176
+ }
177
+ /**
178
+ * Check if node exists
179
+ */
180
+ async nodeExists(id) {
181
+ const response = await this._send('nodeExists', { id: String(id) });
182
+ return response.value;
183
+ }
184
+ /**
185
+ * Find nodes by type
186
+ */
187
+ async findByType(nodeType) {
188
+ const response = await this._send('findByType', { nodeType });
189
+ return response.ids || [];
190
+ }
191
+ /**
192
+ * Find nodes by attributes
193
+ */
194
+ async findByAttr(query) {
195
+ const response = await this._send('findByAttr', { query });
196
+ return response.ids || [];
197
+ }
198
+ // ===========================================================================
199
+ // Graph Traversal
200
+ // ===========================================================================
201
+ /**
202
+ * Get neighbors of a node
203
+ */
204
+ async neighbors(id, edgeTypes = []) {
205
+ const response = await this._send('neighbors', {
206
+ id: String(id),
207
+ edgeTypes
208
+ });
209
+ return response.ids || [];
210
+ }
211
+ /**
212
+ * Breadth-first search
213
+ */
214
+ async bfs(startIds, maxDepth, edgeTypes = []) {
215
+ const response = await this._send('bfs', {
216
+ startIds: startIds.map(String),
217
+ maxDepth,
218
+ edgeTypes
219
+ });
220
+ return response.ids || [];
221
+ }
222
+ /**
223
+ * Depth-first search
224
+ */
225
+ async dfs(startIds, maxDepth, edgeTypes = []) {
226
+ const response = await this._send('dfs', {
227
+ startIds: startIds.map(String),
228
+ maxDepth,
229
+ edgeTypes
230
+ });
231
+ return response.ids || [];
232
+ }
233
+ /**
234
+ * Get outgoing edges from a node
235
+ */
236
+ async getOutgoingEdges(id, edgeTypes = null) {
237
+ const response = await this._send('getOutgoingEdges', {
238
+ id: String(id),
239
+ edgeTypes
240
+ });
241
+ return response.edges || [];
242
+ }
243
+ /**
244
+ * Get incoming edges to a node
245
+ */
246
+ async getIncomingEdges(id, edgeTypes = null) {
247
+ const response = await this._send('getIncomingEdges', {
248
+ id: String(id),
249
+ edgeTypes
250
+ });
251
+ return response.edges || [];
252
+ }
253
+ // ===========================================================================
254
+ // Stats
255
+ // ===========================================================================
256
+ /**
257
+ * Get node count
258
+ */
259
+ async nodeCount() {
260
+ const response = await this._send('nodeCount');
261
+ return response.count;
262
+ }
263
+ /**
264
+ * Get edge count
265
+ */
266
+ async edgeCount() {
267
+ const response = await this._send('edgeCount');
268
+ return response.count;
269
+ }
270
+ /**
271
+ * Count nodes by type
272
+ */
273
+ async countNodesByType(types = null) {
274
+ const response = await this._send('countNodesByType', { types });
275
+ return response.counts || {};
276
+ }
277
+ /**
278
+ * Count edges by type
279
+ */
280
+ async countEdgesByType(edgeTypes = null) {
281
+ const response = await this._send('countEdgesByType', { edgeTypes });
282
+ return response.counts || {};
283
+ }
284
+ // ===========================================================================
285
+ // Control
286
+ // ===========================================================================
287
+ /**
288
+ * Flush data to disk
289
+ */
290
+ async flush() {
291
+ return this._send('flush');
292
+ }
293
+ /**
294
+ * Compact the database
295
+ */
296
+ async compact() {
297
+ return this._send('compact');
298
+ }
299
+ /**
300
+ * Clear the database
301
+ */
302
+ async clear() {
303
+ return this._send('clear');
304
+ }
305
+ // ===========================================================================
306
+ // Bulk Read Operations
307
+ // ===========================================================================
308
+ /**
309
+ * Query nodes (async generator)
310
+ */
311
+ async *queryNodes(query) {
312
+ const serverQuery = {};
313
+ if (query.nodeType)
314
+ serverQuery.nodeType = query.nodeType;
315
+ if (query.type)
316
+ serverQuery.nodeType = query.type;
317
+ if (query.name)
318
+ serverQuery.name = query.name;
319
+ if (query.file)
320
+ serverQuery.file = query.file;
321
+ if (query.exported !== undefined)
322
+ serverQuery.exported = query.exported;
323
+ const response = await this._send('queryNodes', { query: serverQuery });
324
+ const nodes = response.nodes || [];
325
+ for (const node of nodes) {
326
+ yield node;
327
+ }
328
+ }
329
+ /**
330
+ * Get all nodes matching query
331
+ */
332
+ async getAllNodes(query = {}) {
333
+ const nodes = [];
334
+ for await (const node of this.queryNodes(query)) {
335
+ nodes.push(node);
336
+ }
337
+ return nodes;
338
+ }
339
+ /**
340
+ * Get all edges
341
+ */
342
+ async getAllEdges() {
343
+ const response = await this._send('getAllEdges');
344
+ return response.edges || [];
345
+ }
346
+ // ===========================================================================
347
+ // Node Utility Methods
348
+ // ===========================================================================
349
+ /**
350
+ * Check if node is an endpoint (has no outgoing edges)
351
+ */
352
+ async isEndpoint(id) {
353
+ const response = await this._send('isEndpoint', { id: String(id) });
354
+ return response.value;
355
+ }
356
+ /**
357
+ * Get node identifier string
358
+ */
359
+ async getNodeIdentifier(id) {
360
+ const response = await this._send('getNodeIdentifier', { id: String(id) });
361
+ return response.identifier || null;
362
+ }
363
+ /**
364
+ * Update node version
365
+ */
366
+ async updateNodeVersion(id, version) {
367
+ return this._send('updateNodeVersion', { id: String(id), version });
368
+ }
369
+ // ===========================================================================
370
+ // Datalog API
371
+ // ===========================================================================
372
+ /**
373
+ * Load Datalog rules
374
+ */
375
+ async datalogLoadRules(source) {
376
+ const response = await this._send('datalogLoadRules', { source });
377
+ return response.count;
378
+ }
379
+ /**
380
+ * Clear Datalog rules
381
+ */
382
+ async datalogClearRules() {
383
+ return this._send('datalogClearRules');
384
+ }
385
+ /**
386
+ * Execute Datalog query
387
+ */
388
+ async datalogQuery(query) {
389
+ const response = await this._send('datalogQuery', { query });
390
+ return response.results || [];
391
+ }
392
+ /**
393
+ * Check a guarantee (Datalog rule) and return violations
394
+ */
395
+ async checkGuarantee(ruleSource) {
396
+ const response = await this._send('checkGuarantee', { ruleSource });
397
+ return response.violations || [];
398
+ }
399
+ /**
400
+ * Ping the server
401
+ */
402
+ async ping() {
403
+ const response = await this._send('ping');
404
+ return response.pong && response.version ? response.version : false;
405
+ }
406
+ /**
407
+ * Close connection
408
+ */
409
+ async close() {
410
+ if (this.socket) {
411
+ this.socket.destroy();
412
+ this.socket = null;
413
+ this.connected = false;
414
+ }
415
+ }
416
+ /**
417
+ * Shutdown the server
418
+ */
419
+ async shutdown() {
420
+ try {
421
+ await this._send('shutdown');
422
+ }
423
+ catch {
424
+ // Expected - server closes connection
425
+ }
426
+ await this.close();
427
+ }
428
+ }
429
+ export default RFDBClient;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @grafema/rfdb-client - High-performance graph database for code analysis
3
+ *
4
+ * This package provides:
5
+ * - RFDBClient: Socket-based client for out-of-process communication
6
+ * - Protocol types: Wire format types for RFDB communication
7
+ *
8
+ * For NAPI bindings (in-process), see the platform-specific packages.
9
+ */
10
+ export { RFDBClient } from './client.js';
11
+ export type { RFDBCommand, WireNode, WireEdge, RFDBRequest, RFDBResponse, AttrQuery, DatalogResult, IRFDBClient, } from './protocol.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../ts/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,YAAY,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,SAAS,EACT,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @grafema/rfdb-client - High-performance graph database for code analysis
3
+ *
4
+ * This package provides:
5
+ * - RFDBClient: Socket-based client for out-of-process communication
6
+ * - Protocol types: Wire format types for RFDB communication
7
+ *
8
+ * For NAPI bindings (in-process), see the platform-specific packages.
9
+ */
10
+ // Client
11
+ export { RFDBClient } from './client.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * RFDB Protocol Types - re-export from @grafema/types
3
+ *
4
+ * This module provides wire format types for RFDB client-server communication.
5
+ */
6
+ export type { RFDBCommand, WireNode, WireEdge, RFDBRequest, AddNodesRequest, AddEdgesRequest, DeleteNodeRequest, DeleteEdgeRequest, GetNodeRequest, NodeExistsRequest, FindByTypeRequest, FindByAttrRequest, NeighborsRequest, BfsRequest, GetOutgoingEdgesRequest, GetIncomingEdgesRequest, CountNodesByTypeRequest, CountEdgesByTypeRequest, RFDBResponse, AddNodesResponse, AddEdgesResponse, GetNodeResponse, NodeExistsResponse, FindByTypeResponse, FindByAttrResponse, NeighborsResponse, BfsResponse, GetEdgesResponse, CountResponse, CountsByTypeResponse, PingResponse, AttrQuery, DatalogBinding, DatalogResult, IRFDBClient, } from '@grafema/types';
7
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../ts/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAEV,WAAW,EAGX,QAAQ,EACR,QAAQ,EAGR,WAAW,EACX,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EAGvB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,YAAY,EAGZ,SAAS,EACT,cAAc,EACd,aAAa,EAGb,WAAW,GACZ,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * RFDB Protocol Types - re-export from @grafema/types
3
+ *
4
+ * This module provides wire format types for RFDB client-server communication.
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@grafema/rfdb-client",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "TypeScript client for RFDB graph database",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/client.d.ts",
15
+ "import": "./dist/client.js"
16
+ },
17
+ "./protocol": {
18
+ "types": "./dist/protocol.d.ts",
19
+ "import": "./dist/protocol.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "ts"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "clean": "rm -rf dist"
29
+ },
30
+ "keywords": [
31
+ "grafema",
32
+ "graph",
33
+ "database",
34
+ "code-analysis",
35
+ "rust"
36
+ ],
37
+ "license": "Apache-2.0",
38
+ "author": "Vadim Reshetnikov",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/Disentinel/rfdb.git"
42
+ },
43
+ "dependencies": {
44
+ "@grafema/types": "workspace:*",
45
+ "@msgpack/msgpack": "^3.1.3"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.0.8",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "engines": {
52
+ "node": ">= 18"
53
+ }
54
+ }