@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/ts/client.ts ADDED
@@ -0,0 +1,507 @@
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
+
8
+ import { createConnection, Socket } from 'net';
9
+ import { encode, decode } from '@msgpack/msgpack';
10
+ import { EventEmitter } from 'events';
11
+
12
+ import type {
13
+ RFDBCommand,
14
+ WireNode,
15
+ WireEdge,
16
+ RFDBResponse,
17
+ IRFDBClient,
18
+ AttrQuery,
19
+ DatalogResult,
20
+ NodeType,
21
+ EdgeType,
22
+ } from '@grafema/types';
23
+
24
+ interface PendingRequest {
25
+ resolve: (value: RFDBResponse) => void;
26
+ reject: (error: Error) => void;
27
+ }
28
+
29
+ export class RFDBClient extends EventEmitter implements IRFDBClient {
30
+ readonly socketPath: string;
31
+ private socket: Socket | null;
32
+ connected: boolean;
33
+ private pending: Map<number, PendingRequest>;
34
+ private reqId: number;
35
+ private buffer: Buffer;
36
+
37
+ constructor(socketPath: string = '/tmp/rfdb.sock') {
38
+ super();
39
+ this.socketPath = socketPath;
40
+ this.socket = null;
41
+ this.connected = false;
42
+ this.pending = new Map();
43
+ this.reqId = 0;
44
+ this.buffer = Buffer.alloc(0);
45
+ }
46
+
47
+ /**
48
+ * Connect to RFDB server
49
+ */
50
+ async connect(): Promise<void> {
51
+ if (this.connected) return;
52
+
53
+ return new Promise((resolve, reject) => {
54
+ this.socket = createConnection(this.socketPath);
55
+
56
+ this.socket.on('connect', () => {
57
+ this.connected = true;
58
+ this.emit('connected');
59
+ resolve();
60
+ });
61
+
62
+ this.socket.on('error', (err: Error) => {
63
+ if (!this.connected) {
64
+ reject(err);
65
+ } else {
66
+ this.emit('error', err);
67
+ }
68
+ });
69
+
70
+ this.socket.on('close', () => {
71
+ this.connected = false;
72
+ this.emit('disconnected');
73
+ // Reject all pending requests
74
+ for (const [, { reject }] of this.pending) {
75
+ reject(new Error('Connection closed'));
76
+ }
77
+ this.pending.clear();
78
+ });
79
+
80
+ this.socket.on('data', (chunk: Buffer) => {
81
+ this._handleData(chunk);
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Handle incoming data, parse framed messages
88
+ */
89
+ private _handleData(chunk: Buffer): void {
90
+ this.buffer = Buffer.concat([this.buffer, chunk]);
91
+
92
+ while (this.buffer.length >= 4) {
93
+ // Read length prefix (4 bytes, big-endian)
94
+ const msgLen = this.buffer.readUInt32BE(0);
95
+
96
+ if (this.buffer.length < 4 + msgLen) {
97
+ // Not enough data yet
98
+ break;
99
+ }
100
+
101
+ // Extract message
102
+ const msgBytes = this.buffer.subarray(4, 4 + msgLen);
103
+ this.buffer = this.buffer.subarray(4 + msgLen);
104
+
105
+ // Decode and dispatch
106
+ try {
107
+ const response = decode(msgBytes) as RFDBResponse;
108
+ this._handleResponse(response);
109
+ } catch (err) {
110
+ this.emit('error', new Error(`Failed to decode response: ${(err as Error).message}`));
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handle decoded response
117
+ */
118
+ private _handleResponse(response: RFDBResponse): void {
119
+ if (this.pending.size === 0) {
120
+ this.emit('error', new Error('Received response with no pending request'));
121
+ return;
122
+ }
123
+
124
+ // Get the oldest pending request (FIFO)
125
+ const [id, { resolve, reject }] = this.pending.entries().next().value as [number, PendingRequest];
126
+ this.pending.delete(id);
127
+
128
+ if (response.error) {
129
+ reject(new Error(response.error));
130
+ } else {
131
+ resolve(response);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Send a request and wait for response
137
+ */
138
+ private async _send(cmd: RFDBCommand, payload: Record<string, unknown> = {}): Promise<RFDBResponse> {
139
+ if (!this.connected || !this.socket) {
140
+ throw new Error('Not connected to RFDB server');
141
+ }
142
+
143
+ const request = { cmd, ...payload };
144
+ const msgBytes = encode(request);
145
+
146
+ return new Promise((resolve, reject) => {
147
+ const id = this.reqId++;
148
+ this.pending.set(id, { resolve, reject });
149
+
150
+ // Write length prefix + message
151
+ const header = Buffer.alloc(4);
152
+ header.writeUInt32BE(msgBytes.length);
153
+
154
+ this.socket!.write(Buffer.concat([header, Buffer.from(msgBytes)]));
155
+ });
156
+ }
157
+
158
+ // ===========================================================================
159
+ // Write Operations
160
+ // ===========================================================================
161
+
162
+ /**
163
+ * Add nodes to the graph
164
+ */
165
+ async addNodes(nodes: Array<Partial<WireNode> & { id: string; type?: string; node_type?: string; nodeType?: string }>): Promise<RFDBResponse> {
166
+ const wireNodes: WireNode[] = nodes.map(n => ({
167
+ id: String(n.id),
168
+ nodeType: (n.node_type || n.nodeType || n.type || 'UNKNOWN') as NodeType,
169
+ name: n.name || '',
170
+ file: n.file || '',
171
+ exported: n.exported || false,
172
+ metadata: typeof n.metadata === 'string' ? n.metadata : JSON.stringify(n.metadata || {}),
173
+ }));
174
+
175
+ return this._send('addNodes', { nodes: wireNodes });
176
+ }
177
+
178
+ /**
179
+ * Add edges to the graph
180
+ */
181
+ async addEdges(
182
+ edges: Array<Partial<WireEdge> & { src: string; dst: string; type?: string; edge_type?: string; edgeType?: string }>,
183
+ skipValidation: boolean = false
184
+ ): Promise<RFDBResponse> {
185
+ const wireEdges: WireEdge[] = edges.map(e => ({
186
+ src: String(e.src),
187
+ dst: String(e.dst),
188
+ edgeType: (e.edge_type || e.edgeType || e.type || 'UNKNOWN') as EdgeType,
189
+ metadata: typeof e.metadata === 'string' ? e.metadata : JSON.stringify(e.metadata || {}),
190
+ }));
191
+
192
+ return this._send('addEdges', { edges: wireEdges, skipValidation });
193
+ }
194
+
195
+ /**
196
+ * Delete a node
197
+ */
198
+ async deleteNode(id: string): Promise<RFDBResponse> {
199
+ return this._send('deleteNode', { id: String(id) });
200
+ }
201
+
202
+ /**
203
+ * Delete an edge
204
+ */
205
+ async deleteEdge(src: string, dst: string, edgeType: EdgeType): Promise<RFDBResponse> {
206
+ return this._send('deleteEdge', {
207
+ src: String(src),
208
+ dst: String(dst),
209
+ edgeType
210
+ });
211
+ }
212
+
213
+ // ===========================================================================
214
+ // Read Operations
215
+ // ===========================================================================
216
+
217
+ /**
218
+ * Get a node by ID
219
+ */
220
+ async getNode(id: string): Promise<WireNode | null> {
221
+ const response = await this._send('getNode', { id: String(id) });
222
+ return (response as { node?: WireNode }).node || null;
223
+ }
224
+
225
+ /**
226
+ * Check if node exists
227
+ */
228
+ async nodeExists(id: string): Promise<boolean> {
229
+ const response = await this._send('nodeExists', { id: String(id) });
230
+ return (response as { value: boolean }).value;
231
+ }
232
+
233
+ /**
234
+ * Find nodes by type
235
+ */
236
+ async findByType(nodeType: NodeType): Promise<string[]> {
237
+ const response = await this._send('findByType', { nodeType });
238
+ return (response as { ids?: string[] }).ids || [];
239
+ }
240
+
241
+ /**
242
+ * Find nodes by attributes
243
+ */
244
+ async findByAttr(query: Record<string, unknown>): Promise<string[]> {
245
+ const response = await this._send('findByAttr', { query });
246
+ return (response as { ids?: string[] }).ids || [];
247
+ }
248
+
249
+ // ===========================================================================
250
+ // Graph Traversal
251
+ // ===========================================================================
252
+
253
+ /**
254
+ * Get neighbors of a node
255
+ */
256
+ async neighbors(id: string, edgeTypes: EdgeType[] = []): Promise<string[]> {
257
+ const response = await this._send('neighbors', {
258
+ id: String(id),
259
+ edgeTypes
260
+ });
261
+ return (response as { ids?: string[] }).ids || [];
262
+ }
263
+
264
+ /**
265
+ * Breadth-first search
266
+ */
267
+ async bfs(startIds: string[], maxDepth: number, edgeTypes: EdgeType[] = []): Promise<string[]> {
268
+ const response = await this._send('bfs', {
269
+ startIds: startIds.map(String),
270
+ maxDepth,
271
+ edgeTypes
272
+ });
273
+ return (response as { ids?: string[] }).ids || [];
274
+ }
275
+
276
+ /**
277
+ * Depth-first search
278
+ */
279
+ async dfs(startIds: string[], maxDepth: number, edgeTypes: EdgeType[] = []): Promise<string[]> {
280
+ const response = await this._send('dfs', {
281
+ startIds: startIds.map(String),
282
+ maxDepth,
283
+ edgeTypes
284
+ });
285
+ return (response as { ids?: string[] }).ids || [];
286
+ }
287
+
288
+ /**
289
+ * Get outgoing edges from a node
290
+ */
291
+ async getOutgoingEdges(id: string, edgeTypes: EdgeType[] | null = null): Promise<WireEdge[]> {
292
+ const response = await this._send('getOutgoingEdges', {
293
+ id: String(id),
294
+ edgeTypes
295
+ });
296
+ return (response as { edges?: WireEdge[] }).edges || [];
297
+ }
298
+
299
+ /**
300
+ * Get incoming edges to a node
301
+ */
302
+ async getIncomingEdges(id: string, edgeTypes: EdgeType[] | null = null): Promise<WireEdge[]> {
303
+ const response = await this._send('getIncomingEdges', {
304
+ id: String(id),
305
+ edgeTypes
306
+ });
307
+ return (response as { edges?: WireEdge[] }).edges || [];
308
+ }
309
+
310
+ // ===========================================================================
311
+ // Stats
312
+ // ===========================================================================
313
+
314
+ /**
315
+ * Get node count
316
+ */
317
+ async nodeCount(): Promise<number> {
318
+ const response = await this._send('nodeCount');
319
+ return (response as { count: number }).count;
320
+ }
321
+
322
+ /**
323
+ * Get edge count
324
+ */
325
+ async edgeCount(): Promise<number> {
326
+ const response = await this._send('edgeCount');
327
+ return (response as { count: number }).count;
328
+ }
329
+
330
+ /**
331
+ * Count nodes by type
332
+ */
333
+ async countNodesByType(types: NodeType[] | null = null): Promise<Record<string, number>> {
334
+ const response = await this._send('countNodesByType', { types });
335
+ return (response as { counts?: Record<string, number> }).counts || {};
336
+ }
337
+
338
+ /**
339
+ * Count edges by type
340
+ */
341
+ async countEdgesByType(edgeTypes: EdgeType[] | null = null): Promise<Record<string, number>> {
342
+ const response = await this._send('countEdgesByType', { edgeTypes });
343
+ return (response as { counts?: Record<string, number> }).counts || {};
344
+ }
345
+
346
+ // ===========================================================================
347
+ // Control
348
+ // ===========================================================================
349
+
350
+ /**
351
+ * Flush data to disk
352
+ */
353
+ async flush(): Promise<RFDBResponse> {
354
+ return this._send('flush');
355
+ }
356
+
357
+ /**
358
+ * Compact the database
359
+ */
360
+ async compact(): Promise<RFDBResponse> {
361
+ return this._send('compact');
362
+ }
363
+
364
+ /**
365
+ * Clear the database
366
+ */
367
+ async clear(): Promise<RFDBResponse> {
368
+ return this._send('clear');
369
+ }
370
+
371
+ // ===========================================================================
372
+ // Bulk Read Operations
373
+ // ===========================================================================
374
+
375
+ /**
376
+ * Query nodes (async generator)
377
+ */
378
+ async *queryNodes(query: AttrQuery): AsyncGenerator<WireNode, void, unknown> {
379
+ const serverQuery: Record<string, unknown> = {};
380
+ if (query.nodeType) serverQuery.nodeType = query.nodeType;
381
+ if (query.type) serverQuery.nodeType = query.type;
382
+ if (query.name) serverQuery.name = query.name;
383
+ if (query.file) serverQuery.file = query.file;
384
+ if (query.exported !== undefined) serverQuery.exported = query.exported;
385
+
386
+ const response = await this._send('queryNodes', { query: serverQuery });
387
+ const nodes = (response as { nodes?: WireNode[] }).nodes || [];
388
+
389
+ for (const node of nodes) {
390
+ yield node;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Get all nodes matching query
396
+ */
397
+ async getAllNodes(query: AttrQuery = {}): Promise<WireNode[]> {
398
+ const nodes: WireNode[] = [];
399
+ for await (const node of this.queryNodes(query)) {
400
+ nodes.push(node);
401
+ }
402
+ return nodes;
403
+ }
404
+
405
+ /**
406
+ * Get all edges
407
+ */
408
+ async getAllEdges(): Promise<WireEdge[]> {
409
+ const response = await this._send('getAllEdges');
410
+ return (response as { edges?: WireEdge[] }).edges || [];
411
+ }
412
+
413
+ // ===========================================================================
414
+ // Node Utility Methods
415
+ // ===========================================================================
416
+
417
+ /**
418
+ * Check if node is an endpoint (has no outgoing edges)
419
+ */
420
+ async isEndpoint(id: string): Promise<boolean> {
421
+ const response = await this._send('isEndpoint', { id: String(id) });
422
+ return (response as { value: boolean }).value;
423
+ }
424
+
425
+ /**
426
+ * Get node identifier string
427
+ */
428
+ async getNodeIdentifier(id: string): Promise<string | null> {
429
+ const response = await this._send('getNodeIdentifier', { id: String(id) });
430
+ return (response as { identifier?: string | null }).identifier || null;
431
+ }
432
+
433
+ /**
434
+ * Update node version
435
+ */
436
+ async updateNodeVersion(id: string, version: string): Promise<RFDBResponse> {
437
+ return this._send('updateNodeVersion', { id: String(id), version });
438
+ }
439
+
440
+ // ===========================================================================
441
+ // Datalog API
442
+ // ===========================================================================
443
+
444
+ /**
445
+ * Load Datalog rules
446
+ */
447
+ async datalogLoadRules(source: string): Promise<number> {
448
+ const response = await this._send('datalogLoadRules', { source });
449
+ return (response as { count: number }).count;
450
+ }
451
+
452
+ /**
453
+ * Clear Datalog rules
454
+ */
455
+ async datalogClearRules(): Promise<RFDBResponse> {
456
+ return this._send('datalogClearRules');
457
+ }
458
+
459
+ /**
460
+ * Execute Datalog query
461
+ */
462
+ async datalogQuery(query: string): Promise<DatalogResult[]> {
463
+ const response = await this._send('datalogQuery', { query });
464
+ return (response as { results?: DatalogResult[] }).results || [];
465
+ }
466
+
467
+ /**
468
+ * Check a guarantee (Datalog rule) and return violations
469
+ */
470
+ async checkGuarantee(ruleSource: string): Promise<DatalogResult[]> {
471
+ const response = await this._send('checkGuarantee', { ruleSource });
472
+ return (response as { violations?: DatalogResult[] }).violations || [];
473
+ }
474
+
475
+ /**
476
+ * Ping the server
477
+ */
478
+ async ping(): Promise<string | false> {
479
+ const response = await this._send('ping') as { pong?: boolean; version?: string };
480
+ return response.pong && response.version ? response.version : false;
481
+ }
482
+
483
+ /**
484
+ * Close connection
485
+ */
486
+ async close(): Promise<void> {
487
+ if (this.socket) {
488
+ this.socket.destroy();
489
+ this.socket = null;
490
+ this.connected = false;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Shutdown the server
496
+ */
497
+ async shutdown(): Promise<void> {
498
+ try {
499
+ await this._send('shutdown');
500
+ } catch {
501
+ // Expected - server closes connection
502
+ }
503
+ await this.close();
504
+ }
505
+ }
506
+
507
+ export default RFDBClient;
package/ts/index.ts ADDED
@@ -0,0 +1,24 @@
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
+
11
+ // Client
12
+ export { RFDBClient } from './client.js';
13
+
14
+ // Protocol types (re-exported from @grafema/types for convenience)
15
+ export type {
16
+ RFDBCommand,
17
+ WireNode,
18
+ WireEdge,
19
+ RFDBRequest,
20
+ RFDBResponse,
21
+ AttrQuery,
22
+ DatalogResult,
23
+ IRFDBClient,
24
+ } from './protocol.js';
package/ts/protocol.ts ADDED
@@ -0,0 +1,54 @@
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
+
7
+ export type {
8
+ // Commands
9
+ RFDBCommand,
10
+
11
+ // Wire formats
12
+ WireNode,
13
+ WireEdge,
14
+
15
+ // Request types
16
+ RFDBRequest,
17
+ AddNodesRequest,
18
+ AddEdgesRequest,
19
+ DeleteNodeRequest,
20
+ DeleteEdgeRequest,
21
+ GetNodeRequest,
22
+ NodeExistsRequest,
23
+ FindByTypeRequest,
24
+ FindByAttrRequest,
25
+ NeighborsRequest,
26
+ BfsRequest,
27
+ GetOutgoingEdgesRequest,
28
+ GetIncomingEdgesRequest,
29
+ CountNodesByTypeRequest,
30
+ CountEdgesByTypeRequest,
31
+
32
+ // Response types
33
+ RFDBResponse,
34
+ AddNodesResponse,
35
+ AddEdgesResponse,
36
+ GetNodeResponse,
37
+ NodeExistsResponse,
38
+ FindByTypeResponse,
39
+ FindByAttrResponse,
40
+ NeighborsResponse,
41
+ BfsResponse,
42
+ GetEdgesResponse,
43
+ CountResponse,
44
+ CountsByTypeResponse,
45
+ PingResponse,
46
+
47
+ // Query types
48
+ AttrQuery,
49
+ DatalogBinding,
50
+ DatalogResult,
51
+
52
+ // Client interface
53
+ IRFDBClient,
54
+ } from '@grafema/types';