@grafema/rfdb-client 0.2.12-beta → 0.3.1-beta
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/base-client.d.ts +156 -0
- package/dist/base-client.d.ts.map +1 -0
- package/dist/base-client.js +591 -0
- package/dist/base-client.js.map +1 -0
- package/dist/client.d.ts +14 -319
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +20 -740
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/websocket-client.d.ts +41 -0
- package/dist/websocket-client.d.ts.map +1 -0
- package/dist/websocket-client.js +144 -0
- package/dist/websocket-client.js.map +1 -0
- package/package.json +2 -2
- package/ts/base-client.ts +737 -0
- package/ts/client.ts +28 -863
- package/ts/index.ts +2 -0
- package/ts/rfdb-client-locking.test.ts +897 -0
- package/ts/rfdb-websocket-client.test.ts +572 -0
- package/ts/websocket-client.ts +174 -0
package/dist/client.js
CHANGED
|
@@ -6,27 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createConnection } from 'net';
|
|
8
8
|
import { encode, decode } from '@msgpack/msgpack';
|
|
9
|
-
import { EventEmitter } from 'events';
|
|
10
9
|
import { StreamQueue } from './stream-queue.js';
|
|
11
|
-
|
|
10
|
+
import { BaseRFDBClient } from './base-client.js';
|
|
11
|
+
export class RFDBClient extends BaseRFDBClient {
|
|
12
12
|
socketPath;
|
|
13
|
+
clientName;
|
|
13
14
|
socket;
|
|
14
15
|
connected;
|
|
15
16
|
pending;
|
|
16
17
|
reqId;
|
|
17
18
|
buffer;
|
|
18
|
-
// Batch state
|
|
19
|
-
_batching = false;
|
|
20
|
-
_batchNodes = [];
|
|
21
|
-
_batchEdges = [];
|
|
22
|
-
_batchFiles = new Set();
|
|
23
19
|
// Streaming state
|
|
24
20
|
_supportsStreaming = false;
|
|
25
21
|
_pendingStreams = new Map();
|
|
26
22
|
_streamTimers = new Map();
|
|
27
|
-
constructor(socketPath = '/tmp/rfdb.sock') {
|
|
23
|
+
constructor(socketPath = '/tmp/rfdb.sock', clientName = 'unknown') {
|
|
28
24
|
super();
|
|
29
25
|
this.socketPath = socketPath;
|
|
26
|
+
this.clientName = clientName;
|
|
30
27
|
this.socket = null;
|
|
31
28
|
this.connected = false;
|
|
32
29
|
this.pending = new Map();
|
|
@@ -134,7 +131,7 @@ export class RFDBClient extends EventEmitter {
|
|
|
134
131
|
}
|
|
135
132
|
}
|
|
136
133
|
/**
|
|
137
|
-
* Handle decoded response
|
|
134
|
+
* Handle decoded response -- match by requestId, route streaming chunks
|
|
138
135
|
* to StreamQueue or resolve single-response Promise.
|
|
139
136
|
*/
|
|
140
137
|
_handleResponse(response) {
|
|
@@ -167,7 +164,7 @@ export class RFDBClient extends EventEmitter {
|
|
|
167
164
|
this._handleStreamingResponse(id, response, streamQueue);
|
|
168
165
|
return;
|
|
169
166
|
}
|
|
170
|
-
// Non-streaming response
|
|
167
|
+
// Non-streaming response -- existing behavior
|
|
171
168
|
if (!this.pending.has(id)) {
|
|
172
169
|
this.emit('error', new Error(`Received response for unknown requestId: ${response.requestId}`));
|
|
173
170
|
return;
|
|
@@ -183,17 +180,13 @@ export class RFDBClient extends EventEmitter {
|
|
|
183
180
|
}
|
|
184
181
|
/**
|
|
185
182
|
* Handle a response for a streaming request.
|
|
186
|
-
* Routes chunk data to StreamQueue and manages stream lifecycle.
|
|
187
|
-
* Resets per-chunk timeout on each successful chunk arrival.
|
|
188
183
|
*/
|
|
189
184
|
_handleStreamingResponse(id, response, streamQueue) {
|
|
190
|
-
// Error response — fail the stream
|
|
191
185
|
if (response.error) {
|
|
192
186
|
this._cleanupStream(id);
|
|
193
187
|
streamQueue.fail(new Error(response.error));
|
|
194
188
|
return;
|
|
195
189
|
}
|
|
196
|
-
// Streaming chunk (has `done` field)
|
|
197
190
|
if ('done' in response) {
|
|
198
191
|
const chunk = response;
|
|
199
192
|
const nodes = chunk.nodes || [];
|
|
@@ -205,13 +198,11 @@ export class RFDBClient extends EventEmitter {
|
|
|
205
198
|
streamQueue.end();
|
|
206
199
|
}
|
|
207
200
|
else {
|
|
208
|
-
// Reset per-chunk timeout
|
|
209
201
|
this._resetStreamTimer(id, streamQueue);
|
|
210
202
|
}
|
|
211
203
|
return;
|
|
212
204
|
}
|
|
213
205
|
// Auto-fallback: server sent a non-streaming Nodes response
|
|
214
|
-
// (server doesn't support streaming or result was below threshold)
|
|
215
206
|
const nodesResponse = response;
|
|
216
207
|
const nodes = nodesResponse.nodes || [];
|
|
217
208
|
for (const node of nodes) {
|
|
@@ -220,9 +211,6 @@ export class RFDBClient extends EventEmitter {
|
|
|
220
211
|
this._cleanupStream(id);
|
|
221
212
|
streamQueue.end();
|
|
222
213
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Reset the per-chunk timeout for a streaming request.
|
|
225
|
-
*/
|
|
226
214
|
_resetStreamTimer(id, streamQueue) {
|
|
227
215
|
const existing = this._streamTimers.get(id);
|
|
228
216
|
if (existing)
|
|
@@ -233,9 +221,6 @@ export class RFDBClient extends EventEmitter {
|
|
|
233
221
|
}, RFDBClient.DEFAULT_TIMEOUT_MS);
|
|
234
222
|
this._streamTimers.set(id, timer);
|
|
235
223
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Clean up all state for a completed/failed streaming request.
|
|
238
|
-
*/
|
|
239
224
|
_cleanupStream(id) {
|
|
240
225
|
this._pendingStreams.delete(id);
|
|
241
226
|
this.pending.delete(id);
|
|
@@ -251,10 +236,6 @@ export class RFDBClient extends EventEmitter {
|
|
|
251
236
|
const num = parseInt(requestId.slice(1), 10);
|
|
252
237
|
return Number.isNaN(num) ? null : num;
|
|
253
238
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Default timeout for operations (60 seconds)
|
|
256
|
-
* Flush/compact may take time for large graphs, but should not hang indefinitely
|
|
257
|
-
*/
|
|
258
239
|
static DEFAULT_TIMEOUT_MS = 60_000;
|
|
259
240
|
/**
|
|
260
241
|
* Send a request and wait for response with timeout
|
|
@@ -267,12 +248,10 @@ export class RFDBClient extends EventEmitter {
|
|
|
267
248
|
const id = this.reqId++;
|
|
268
249
|
const request = { requestId: `r${id}`, cmd, ...payload };
|
|
269
250
|
const msgBytes = encode(request);
|
|
270
|
-
// Setup timeout
|
|
271
251
|
const timer = setTimeout(() => {
|
|
272
252
|
this.pending.delete(id);
|
|
273
253
|
reject(new Error(`RFDB ${cmd} timed out after ${timeoutMs}ms. Server may be unresponsive or dbPath may be invalid.`));
|
|
274
254
|
}, timeoutMs);
|
|
275
|
-
// Handle socket errors during this request
|
|
276
255
|
const errorHandler = (err) => {
|
|
277
256
|
this.pending.delete(id);
|
|
278
257
|
clearTimeout(timer);
|
|
@@ -289,7 +268,7 @@ export class RFDBClient extends EventEmitter {
|
|
|
289
268
|
clearTimeout(timer);
|
|
290
269
|
this.socket?.removeListener('error', errorHandler);
|
|
291
270
|
reject(error);
|
|
292
|
-
}
|
|
271
|
+
},
|
|
293
272
|
});
|
|
294
273
|
// Write length prefix + message
|
|
295
274
|
const header = Buffer.alloc(4);
|
|
@@ -298,271 +277,23 @@ export class RFDBClient extends EventEmitter {
|
|
|
298
277
|
});
|
|
299
278
|
}
|
|
300
279
|
// ===========================================================================
|
|
301
|
-
//
|
|
302
|
-
// ===========================================================================
|
|
303
|
-
/**
|
|
304
|
-
* Add nodes to the graph
|
|
305
|
-
* Extra properties beyond id/type/name/file/exported/metadata are merged into metadata
|
|
306
|
-
*/
|
|
307
|
-
async addNodes(nodes) {
|
|
308
|
-
const wireNodes = nodes.map(n => {
|
|
309
|
-
// Cast to Record to allow iteration over extra properties
|
|
310
|
-
const nodeRecord = n;
|
|
311
|
-
// Extract known wire format fields, rest goes to metadata
|
|
312
|
-
const { id, type, node_type, nodeType, name, file, exported, metadata, semanticId, semantic_id, ...rest } = nodeRecord;
|
|
313
|
-
// Merge explicit metadata with extra properties
|
|
314
|
-
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata) : (metadata || {});
|
|
315
|
-
const combinedMeta = { ...existingMeta, ...rest };
|
|
316
|
-
const wire = {
|
|
317
|
-
id: String(id),
|
|
318
|
-
nodeType: (node_type || nodeType || type || 'UNKNOWN'),
|
|
319
|
-
name: name || '',
|
|
320
|
-
file: file || '',
|
|
321
|
-
exported: exported || false,
|
|
322
|
-
metadata: JSON.stringify(combinedMeta),
|
|
323
|
-
};
|
|
324
|
-
// Preserve semanticId as top-level field for v3 protocol
|
|
325
|
-
const sid = semanticId || semantic_id;
|
|
326
|
-
if (sid) {
|
|
327
|
-
wire.semanticId = String(sid);
|
|
328
|
-
}
|
|
329
|
-
return wire;
|
|
330
|
-
});
|
|
331
|
-
if (this._batching) {
|
|
332
|
-
this._batchNodes.push(...wireNodes);
|
|
333
|
-
for (const node of wireNodes) {
|
|
334
|
-
if (node.file)
|
|
335
|
-
this._batchFiles.add(node.file);
|
|
336
|
-
}
|
|
337
|
-
return { ok: true };
|
|
338
|
-
}
|
|
339
|
-
return this._send('addNodes', { nodes: wireNodes });
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Add edges to the graph
|
|
343
|
-
* Extra properties beyond src/dst/type are merged into metadata
|
|
344
|
-
*/
|
|
345
|
-
async addEdges(edges, skipValidation = false) {
|
|
346
|
-
const wireEdges = edges.map(e => {
|
|
347
|
-
// Cast to unknown first then to Record to allow extra properties
|
|
348
|
-
const edge = e;
|
|
349
|
-
// Extract known fields, rest goes to metadata
|
|
350
|
-
const { src, dst, type, edge_type, edgeType, metadata, ...rest } = edge;
|
|
351
|
-
// Merge explicit metadata with extra properties
|
|
352
|
-
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata) : (metadata || {});
|
|
353
|
-
const combinedMeta = { ...existingMeta, ...rest };
|
|
354
|
-
return {
|
|
355
|
-
src: String(src),
|
|
356
|
-
dst: String(dst),
|
|
357
|
-
edgeType: (edge_type || edgeType || type || e.edgeType || 'UNKNOWN'),
|
|
358
|
-
metadata: JSON.stringify(combinedMeta),
|
|
359
|
-
};
|
|
360
|
-
});
|
|
361
|
-
if (this._batching) {
|
|
362
|
-
this._batchEdges.push(...wireEdges);
|
|
363
|
-
return { ok: true };
|
|
364
|
-
}
|
|
365
|
-
return this._send('addEdges', { edges: wireEdges, skipValidation });
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Delete a node
|
|
369
|
-
*/
|
|
370
|
-
async deleteNode(id) {
|
|
371
|
-
return this._send('deleteNode', { id: String(id) });
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Delete an edge
|
|
375
|
-
*/
|
|
376
|
-
async deleteEdge(src, dst, edgeType) {
|
|
377
|
-
return this._send('deleteEdge', {
|
|
378
|
-
src: String(src),
|
|
379
|
-
dst: String(dst),
|
|
380
|
-
edgeType
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
// ===========================================================================
|
|
384
|
-
// Read Operations
|
|
280
|
+
// Streaming Overrides (Unix socket supports streaming)
|
|
385
281
|
// ===========================================================================
|
|
386
282
|
/**
|
|
387
|
-
*
|
|
283
|
+
* Negotiate protocol version with server.
|
|
284
|
+
* Overrides base to set streaming flag.
|
|
388
285
|
*/
|
|
389
|
-
async
|
|
390
|
-
const response = await this._send('
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
* Check if node exists
|
|
395
|
-
*/
|
|
396
|
-
async nodeExists(id) {
|
|
397
|
-
const response = await this._send('nodeExists', { id: String(id) });
|
|
398
|
-
return response.value;
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Find nodes by type
|
|
402
|
-
*/
|
|
403
|
-
async findByType(nodeType) {
|
|
404
|
-
const response = await this._send('findByType', { nodeType });
|
|
405
|
-
return response.ids || [];
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Find nodes by attributes
|
|
409
|
-
*/
|
|
410
|
-
async findByAttr(query) {
|
|
411
|
-
const response = await this._send('findByAttr', { query });
|
|
412
|
-
return response.ids || [];
|
|
413
|
-
}
|
|
414
|
-
// ===========================================================================
|
|
415
|
-
// Graph Traversal
|
|
416
|
-
// ===========================================================================
|
|
417
|
-
/**
|
|
418
|
-
* Get neighbors of a node
|
|
419
|
-
*/
|
|
420
|
-
async neighbors(id, edgeTypes = []) {
|
|
421
|
-
const response = await this._send('neighbors', {
|
|
422
|
-
id: String(id),
|
|
423
|
-
edgeTypes
|
|
424
|
-
});
|
|
425
|
-
return response.ids || [];
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Breadth-first search
|
|
429
|
-
*/
|
|
430
|
-
async bfs(startIds, maxDepth, edgeTypes = []) {
|
|
431
|
-
const response = await this._send('bfs', {
|
|
432
|
-
startIds: startIds.map(String),
|
|
433
|
-
maxDepth,
|
|
434
|
-
edgeTypes
|
|
435
|
-
});
|
|
436
|
-
return response.ids || [];
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Depth-first search
|
|
440
|
-
*/
|
|
441
|
-
async dfs(startIds, maxDepth, edgeTypes = []) {
|
|
442
|
-
const response = await this._send('dfs', {
|
|
443
|
-
startIds: startIds.map(String),
|
|
444
|
-
maxDepth,
|
|
445
|
-
edgeTypes
|
|
446
|
-
});
|
|
447
|
-
return response.ids || [];
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Reachability query - find all nodes reachable from start nodes
|
|
451
|
-
*/
|
|
452
|
-
async reachability(startIds, maxDepth, edgeTypes = [], backward = false) {
|
|
453
|
-
const response = await this._send('reachability', {
|
|
454
|
-
startIds: startIds.map(String),
|
|
455
|
-
maxDepth,
|
|
456
|
-
edgeTypes,
|
|
457
|
-
backward
|
|
458
|
-
});
|
|
459
|
-
return response.ids || [];
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Get outgoing edges from a node
|
|
463
|
-
* Parses metadata JSON and spreads it onto the edge object for convenience
|
|
464
|
-
*/
|
|
465
|
-
async getOutgoingEdges(id, edgeTypes = null) {
|
|
466
|
-
const response = await this._send('getOutgoingEdges', {
|
|
467
|
-
id: String(id),
|
|
468
|
-
edgeTypes
|
|
469
|
-
});
|
|
470
|
-
const edges = response.edges || [];
|
|
471
|
-
// Parse metadata and spread onto edge for convenience
|
|
472
|
-
return edges.map(e => {
|
|
473
|
-
let meta = {};
|
|
474
|
-
try {
|
|
475
|
-
meta = e.metadata ? JSON.parse(e.metadata) : {};
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
// Keep empty metadata on parse error
|
|
479
|
-
}
|
|
480
|
-
return { ...e, type: e.edgeType, ...meta };
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Get incoming edges to a node
|
|
485
|
-
* Parses metadata JSON and spreads it onto the edge object for convenience
|
|
486
|
-
*/
|
|
487
|
-
async getIncomingEdges(id, edgeTypes = null) {
|
|
488
|
-
const response = await this._send('getIncomingEdges', {
|
|
489
|
-
id: String(id),
|
|
490
|
-
edgeTypes
|
|
491
|
-
});
|
|
492
|
-
const edges = response.edges || [];
|
|
493
|
-
// Parse metadata and spread onto edge for convenience
|
|
494
|
-
return edges.map(e => {
|
|
495
|
-
let meta = {};
|
|
496
|
-
try {
|
|
497
|
-
meta = e.metadata ? JSON.parse(e.metadata) : {};
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
// Keep empty metadata on parse error
|
|
501
|
-
}
|
|
502
|
-
return { ...e, type: e.edgeType, ...meta };
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
// ===========================================================================
|
|
506
|
-
// Stats
|
|
507
|
-
// ===========================================================================
|
|
508
|
-
/**
|
|
509
|
-
* Get node count
|
|
510
|
-
*/
|
|
511
|
-
async nodeCount() {
|
|
512
|
-
const response = await this._send('nodeCount');
|
|
513
|
-
return response.count;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Get edge count
|
|
517
|
-
*/
|
|
518
|
-
async edgeCount() {
|
|
519
|
-
const response = await this._send('edgeCount');
|
|
520
|
-
return response.count;
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Count nodes by type
|
|
524
|
-
*/
|
|
525
|
-
async countNodesByType(types = null) {
|
|
526
|
-
const response = await this._send('countNodesByType', { types });
|
|
527
|
-
return response.counts || {};
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Count edges by type
|
|
531
|
-
*/
|
|
532
|
-
async countEdgesByType(edgeTypes = null) {
|
|
533
|
-
const response = await this._send('countEdgesByType', { edgeTypes });
|
|
534
|
-
return response.counts || {};
|
|
535
|
-
}
|
|
536
|
-
// ===========================================================================
|
|
537
|
-
// Control
|
|
538
|
-
// ===========================================================================
|
|
539
|
-
/**
|
|
540
|
-
* Flush data to disk
|
|
541
|
-
*/
|
|
542
|
-
async flush() {
|
|
543
|
-
return this._send('flush');
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Compact the database
|
|
547
|
-
*/
|
|
548
|
-
async compact() {
|
|
549
|
-
return this._send('compact');
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Clear the database
|
|
553
|
-
*/
|
|
554
|
-
async clear() {
|
|
555
|
-
return this._send('clear');
|
|
286
|
+
async hello(protocolVersion = 3) {
|
|
287
|
+
const response = await this._send('hello', { protocolVersion });
|
|
288
|
+
const hello = response;
|
|
289
|
+
this._supportsStreaming = hello.features?.includes('streaming') ?? false;
|
|
290
|
+
return hello;
|
|
556
291
|
}
|
|
557
|
-
// ===========================================================================
|
|
558
|
-
// Bulk Read Operations
|
|
559
|
-
// ===========================================================================
|
|
560
292
|
/**
|
|
561
|
-
* Query nodes (async generator)
|
|
293
|
+
* Query nodes (async generator).
|
|
294
|
+
* Overrides base to support streaming for protocol v3+.
|
|
562
295
|
*/
|
|
563
296
|
async *queryNodes(query) {
|
|
564
|
-
// When server supports streaming (protocol v3+), delegate to streaming handler
|
|
565
|
-
// to correctly handle chunked NodesChunk responses for large result sets.
|
|
566
297
|
if (this._supportsStreaming) {
|
|
567
298
|
yield* this.queryNodesStream(query);
|
|
568
299
|
return;
|
|
@@ -574,37 +305,13 @@ export class RFDBClient extends EventEmitter {
|
|
|
574
305
|
yield node;
|
|
575
306
|
}
|
|
576
307
|
}
|
|
577
|
-
/**
|
|
578
|
-
* Build a server query object from an AttrQuery.
|
|
579
|
-
*/
|
|
580
|
-
_buildServerQuery(query) {
|
|
581
|
-
const serverQuery = {};
|
|
582
|
-
if (query.nodeType)
|
|
583
|
-
serverQuery.nodeType = query.nodeType;
|
|
584
|
-
if (query.type)
|
|
585
|
-
serverQuery.nodeType = query.type;
|
|
586
|
-
if (query.name)
|
|
587
|
-
serverQuery.name = query.name;
|
|
588
|
-
if (query.file)
|
|
589
|
-
serverQuery.file = query.file;
|
|
590
|
-
if (query.exported !== undefined)
|
|
591
|
-
serverQuery.exported = query.exported;
|
|
592
|
-
return serverQuery;
|
|
593
|
-
}
|
|
594
308
|
/**
|
|
595
309
|
* Stream nodes matching query with true streaming support.
|
|
596
|
-
*
|
|
597
|
-
* Behavior depends on server capabilities:
|
|
598
|
-
* - Server supports streaming (protocol v3): receives chunked NodesChunk
|
|
599
|
-
* responses via StreamQueue. Nodes are yielded as they arrive.
|
|
600
|
-
* - Server does NOT support streaming (fallback): delegates to queryNodes()
|
|
601
|
-
* which yields nodes one by one from bulk response.
|
|
602
|
-
*
|
|
603
|
-
* The generator can be aborted by breaking out of the loop or calling .return().
|
|
310
|
+
* Overrides base to use StreamQueue for protocol v3+.
|
|
604
311
|
*/
|
|
605
312
|
async *queryNodesStream(query) {
|
|
606
313
|
if (!this._supportsStreaming) {
|
|
607
|
-
yield*
|
|
314
|
+
yield* super.queryNodes(query);
|
|
608
315
|
return;
|
|
609
316
|
}
|
|
610
317
|
if (!this.connected || !this.socket) {
|
|
@@ -614,12 +321,10 @@ export class RFDBClient extends EventEmitter {
|
|
|
614
321
|
const id = this.reqId++;
|
|
615
322
|
const streamQueue = new StreamQueue();
|
|
616
323
|
this._pendingStreams.set(id, streamQueue);
|
|
617
|
-
// Build and send request manually (can't use _send which expects single response)
|
|
618
324
|
const request = { requestId: `r${id}`, cmd: 'queryNodes', query: serverQuery };
|
|
619
325
|
const msgBytes = encode(request);
|
|
620
326
|
const header = Buffer.alloc(4);
|
|
621
327
|
header.writeUInt32BE(msgBytes.length);
|
|
622
|
-
// Register in pending map for error routing
|
|
623
328
|
this.pending.set(id, {
|
|
624
329
|
resolve: () => { this._cleanupStream(id); },
|
|
625
330
|
reject: (error) => {
|
|
@@ -627,7 +332,6 @@ export class RFDBClient extends EventEmitter {
|
|
|
627
332
|
streamQueue.fail(error);
|
|
628
333
|
},
|
|
629
334
|
});
|
|
630
|
-
// Start per-chunk timeout (resets on each chunk in _handleStreamingResponse)
|
|
631
335
|
this._resetStreamTimer(id, streamQueue);
|
|
632
336
|
this.socket.write(Buffer.concat([header, Buffer.from(msgBytes)]));
|
|
633
337
|
try {
|
|
@@ -639,421 +343,14 @@ export class RFDBClient extends EventEmitter {
|
|
|
639
343
|
this._cleanupStream(id);
|
|
640
344
|
}
|
|
641
345
|
}
|
|
642
|
-
/**
|
|
643
|
-
* Get all nodes matching query
|
|
644
|
-
*/
|
|
645
|
-
async getAllNodes(query = {}) {
|
|
646
|
-
const nodes = [];
|
|
647
|
-
for await (const node of this.queryNodes(query)) {
|
|
648
|
-
nodes.push(node);
|
|
649
|
-
}
|
|
650
|
-
return nodes;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Get all edges
|
|
654
|
-
* Parses metadata JSON and spreads it onto the edge object for convenience
|
|
655
|
-
*/
|
|
656
|
-
async getAllEdges() {
|
|
657
|
-
const response = await this._send('getAllEdges');
|
|
658
|
-
const edges = response.edges || [];
|
|
659
|
-
// Parse metadata and spread onto edge for convenience
|
|
660
|
-
return edges.map(e => {
|
|
661
|
-
let meta = {};
|
|
662
|
-
try {
|
|
663
|
-
meta = e.metadata ? JSON.parse(e.metadata) : {};
|
|
664
|
-
}
|
|
665
|
-
catch {
|
|
666
|
-
// Keep empty metadata on parse error
|
|
667
|
-
}
|
|
668
|
-
return { ...e, type: e.edgeType, ...meta };
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
// ===========================================================================
|
|
672
|
-
// Node Utility Methods
|
|
673
|
-
// ===========================================================================
|
|
674
|
-
/**
|
|
675
|
-
* Check if node is an endpoint (has no outgoing edges)
|
|
676
|
-
*/
|
|
677
|
-
async isEndpoint(id) {
|
|
678
|
-
const response = await this._send('isEndpoint', { id: String(id) });
|
|
679
|
-
return response.value;
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Get node identifier string
|
|
683
|
-
*/
|
|
684
|
-
async getNodeIdentifier(id) {
|
|
685
|
-
const response = await this._send('getNodeIdentifier', { id: String(id) });
|
|
686
|
-
return response.identifier || null;
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Update node version
|
|
690
|
-
*/
|
|
691
|
-
async updateNodeVersion(id, version) {
|
|
692
|
-
return this._send('updateNodeVersion', { id: String(id), version });
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Declare metadata fields for server-side indexing.
|
|
696
|
-
* Call before adding nodes so the server builds indexes on flush.
|
|
697
|
-
* Returns the number of declared fields.
|
|
698
|
-
*/
|
|
699
|
-
async declareFields(fields) {
|
|
700
|
-
const response = await this._send('declareFields', { fields });
|
|
701
|
-
return response.count || 0;
|
|
702
|
-
}
|
|
703
|
-
// ===========================================================================
|
|
704
|
-
// Datalog API
|
|
705
|
-
// ===========================================================================
|
|
706
|
-
/**
|
|
707
|
-
* Load Datalog rules
|
|
708
|
-
*/
|
|
709
|
-
async datalogLoadRules(source) {
|
|
710
|
-
const response = await this._send('datalogLoadRules', { source });
|
|
711
|
-
return response.count;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Clear Datalog rules
|
|
715
|
-
*/
|
|
716
|
-
async datalogClearRules() {
|
|
717
|
-
return this._send('datalogClearRules');
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Execute Datalog query
|
|
721
|
-
*/
|
|
722
|
-
async datalogQuery(query) {
|
|
723
|
-
const response = await this._send('datalogQuery', { query });
|
|
724
|
-
return response.results || [];
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Check a guarantee (Datalog rule) and return violations
|
|
728
|
-
*/
|
|
729
|
-
async checkGuarantee(ruleSource) {
|
|
730
|
-
const response = await this._send('checkGuarantee', { ruleSource });
|
|
731
|
-
return response.violations || [];
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* Execute unified Datalog — handles both direct queries and rule-based programs.
|
|
735
|
-
* Auto-detects the head predicate instead of hardcoding violation(X).
|
|
736
|
-
*/
|
|
737
|
-
async executeDatalog(source) {
|
|
738
|
-
const response = await this._send('executeDatalog', { source });
|
|
739
|
-
return response.results || [];
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Ping the server
|
|
743
|
-
*/
|
|
744
|
-
async ping() {
|
|
745
|
-
const response = await this._send('ping');
|
|
746
|
-
return response.pong && response.version ? response.version : false;
|
|
747
|
-
}
|
|
748
|
-
// ===========================================================================
|
|
749
|
-
// Protocol v2 - Multi-Database Commands
|
|
750
|
-
// ===========================================================================
|
|
751
|
-
/**
|
|
752
|
-
* Negotiate protocol version with server
|
|
753
|
-
* @param protocolVersion - Protocol version to negotiate (default: 2)
|
|
754
|
-
* @returns Server capabilities including protocolVersion, serverVersion, features
|
|
755
|
-
*/
|
|
756
|
-
async hello(protocolVersion = 3) {
|
|
757
|
-
const response = await this._send('hello', { protocolVersion });
|
|
758
|
-
const hello = response;
|
|
759
|
-
this._supportsStreaming = hello.features?.includes('streaming') ?? false;
|
|
760
|
-
return hello;
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Create a new database
|
|
764
|
-
* @param name - Database name (alphanumeric, _, -)
|
|
765
|
-
* @param ephemeral - If true, database is in-memory and auto-cleaned on disconnect
|
|
766
|
-
*/
|
|
767
|
-
async createDatabase(name, ephemeral = false) {
|
|
768
|
-
const response = await this._send('createDatabase', { name, ephemeral });
|
|
769
|
-
return response;
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Open a database and set as current for this session
|
|
773
|
-
* @param name - Database name
|
|
774
|
-
* @param mode - 'rw' (read-write) or 'ro' (read-only)
|
|
775
|
-
*/
|
|
776
|
-
async openDatabase(name, mode = 'rw') {
|
|
777
|
-
const response = await this._send('openDatabase', { name, mode });
|
|
778
|
-
return response;
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Close current database
|
|
782
|
-
*/
|
|
783
|
-
async closeDatabase() {
|
|
784
|
-
return this._send('closeDatabase');
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Drop (delete) a database - must not be in use
|
|
788
|
-
* @param name - Database name
|
|
789
|
-
*/
|
|
790
|
-
async dropDatabase(name) {
|
|
791
|
-
return this._send('dropDatabase', { name });
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* List all databases
|
|
795
|
-
*/
|
|
796
|
-
async listDatabases() {
|
|
797
|
-
const response = await this._send('listDatabases');
|
|
798
|
-
return response;
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Get current database for this session
|
|
802
|
-
*/
|
|
803
|
-
async currentDatabase() {
|
|
804
|
-
const response = await this._send('currentDatabase');
|
|
805
|
-
return response;
|
|
806
|
-
}
|
|
807
|
-
// ===========================================================================
|
|
808
|
-
// Snapshot Operations
|
|
809
|
-
// ===========================================================================
|
|
810
|
-
/**
|
|
811
|
-
* Convert a SnapshotRef to wire format payload fields.
|
|
812
|
-
*
|
|
813
|
-
* - number -> { version: N }
|
|
814
|
-
* - { tag, value } -> { tagKey, tagValue }
|
|
815
|
-
*/
|
|
816
|
-
_resolveSnapshotRef(ref) {
|
|
817
|
-
if (typeof ref === 'number')
|
|
818
|
-
return { version: ref };
|
|
819
|
-
return { tagKey: ref.tag, tagValue: ref.value };
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* Compute diff between two snapshots.
|
|
823
|
-
* @param from - Source snapshot (version number or tag reference)
|
|
824
|
-
* @param to - Target snapshot (version number or tag reference)
|
|
825
|
-
* @returns SnapshotDiff with added/removed segments and stats
|
|
826
|
-
*/
|
|
827
|
-
async diffSnapshots(from, to) {
|
|
828
|
-
const response = await this._send('diffSnapshots', {
|
|
829
|
-
from: this._resolveSnapshotRef(from),
|
|
830
|
-
to: this._resolveSnapshotRef(to),
|
|
831
|
-
});
|
|
832
|
-
return response.diff;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Tag a snapshot with key-value metadata.
|
|
836
|
-
* @param version - Snapshot version to tag
|
|
837
|
-
* @param tags - Key-value pairs to apply (e.g. { "release": "v1.0" })
|
|
838
|
-
*/
|
|
839
|
-
async tagSnapshot(version, tags) {
|
|
840
|
-
await this._send('tagSnapshot', { version, tags });
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* Find a snapshot by tag key/value pair.
|
|
844
|
-
* @param tagKey - Tag key to search for
|
|
845
|
-
* @param tagValue - Tag value to match
|
|
846
|
-
* @returns Snapshot version number, or null if not found
|
|
847
|
-
*/
|
|
848
|
-
async findSnapshot(tagKey, tagValue) {
|
|
849
|
-
const response = await this._send('findSnapshot', { tagKey, tagValue });
|
|
850
|
-
return response.version;
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* List snapshots, optionally filtered by tag key.
|
|
854
|
-
* @param filterTag - Optional tag key to filter by (only snapshots with this tag)
|
|
855
|
-
* @returns Array of SnapshotInfo objects
|
|
856
|
-
*/
|
|
857
|
-
async listSnapshots(filterTag) {
|
|
858
|
-
const payload = {};
|
|
859
|
-
if (filterTag !== undefined)
|
|
860
|
-
payload.filterTag = filterTag;
|
|
861
|
-
const response = await this._send('listSnapshots', payload);
|
|
862
|
-
return response.snapshots;
|
|
863
|
-
}
|
|
864
|
-
// ===========================================================================
|
|
865
|
-
// Batch Operations
|
|
866
|
-
// ===========================================================================
|
|
867
|
-
/**
|
|
868
|
-
* Begin a batch operation.
|
|
869
|
-
* While batching, addNodes/addEdges buffer locally instead of sending to server.
|
|
870
|
-
* Call commitBatch() to send all buffered data atomically.
|
|
871
|
-
*/
|
|
872
|
-
beginBatch() {
|
|
873
|
-
if (this._batching)
|
|
874
|
-
throw new Error('Batch already in progress');
|
|
875
|
-
this._batching = true;
|
|
876
|
-
this._batchNodes = [];
|
|
877
|
-
this._batchEdges = [];
|
|
878
|
-
this._batchFiles = new Set();
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Synchronously batch a single node. Must be inside beginBatch/commitBatch.
|
|
882
|
-
* Skips async wrapper — pushes directly to batch array.
|
|
883
|
-
*/
|
|
884
|
-
batchNode(node) {
|
|
885
|
-
if (!this._batching)
|
|
886
|
-
throw new Error('No batch in progress');
|
|
887
|
-
const nodeRecord = node;
|
|
888
|
-
const { id, type, node_type, nodeType, name, file, exported, metadata, semanticId, semantic_id, ...rest } = nodeRecord;
|
|
889
|
-
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata) : (metadata || {});
|
|
890
|
-
const combinedMeta = { ...existingMeta, ...rest };
|
|
891
|
-
const wire = {
|
|
892
|
-
id: String(id),
|
|
893
|
-
nodeType: (node_type || nodeType || type || 'UNKNOWN'),
|
|
894
|
-
name: name || '',
|
|
895
|
-
file: file || '',
|
|
896
|
-
exported: exported || false,
|
|
897
|
-
metadata: JSON.stringify(combinedMeta),
|
|
898
|
-
};
|
|
899
|
-
const sid = semanticId || semantic_id;
|
|
900
|
-
if (sid) {
|
|
901
|
-
wire.semanticId = String(sid);
|
|
902
|
-
}
|
|
903
|
-
this._batchNodes.push(wire);
|
|
904
|
-
if (wire.file)
|
|
905
|
-
this._batchFiles.add(wire.file);
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Synchronously batch a single edge. Must be inside beginBatch/commitBatch.
|
|
909
|
-
*/
|
|
910
|
-
batchEdge(edge) {
|
|
911
|
-
if (!this._batching)
|
|
912
|
-
throw new Error('No batch in progress');
|
|
913
|
-
const edgeRecord = edge;
|
|
914
|
-
const { src, dst, type, edge_type, edgeType, metadata, ...rest } = edgeRecord;
|
|
915
|
-
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata) : (metadata || {});
|
|
916
|
-
const combinedMeta = { ...existingMeta, ...rest };
|
|
917
|
-
this._batchEdges.push({
|
|
918
|
-
src: String(src),
|
|
919
|
-
dst: String(dst),
|
|
920
|
-
edgeType: (edge_type || edgeType || type || edge.edgeType || 'UNKNOWN'),
|
|
921
|
-
metadata: JSON.stringify(combinedMeta),
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Commit the current batch to the server.
|
|
926
|
-
* Sends all buffered nodes/edges with the list of changed files.
|
|
927
|
-
* Server atomically replaces old data for changed files with new data.
|
|
928
|
-
*
|
|
929
|
-
* @param tags - Optional tags for the commit (e.g., plugin name, phase)
|
|
930
|
-
* @param deferIndex - When true, server writes data but skips index rebuild.
|
|
931
|
-
* Caller must send rebuildIndexes() after all deferred commits complete.
|
|
932
|
-
*/
|
|
933
|
-
async commitBatch(tags, deferIndex, protectedTypes) {
|
|
934
|
-
if (!this._batching)
|
|
935
|
-
throw new Error('No batch in progress');
|
|
936
|
-
const allNodes = this._batchNodes;
|
|
937
|
-
const allEdges = this._batchEdges;
|
|
938
|
-
const changedFiles = [...this._batchFiles];
|
|
939
|
-
this._batching = false;
|
|
940
|
-
this._batchNodes = [];
|
|
941
|
-
this._batchEdges = [];
|
|
942
|
-
this._batchFiles = new Set();
|
|
943
|
-
return this._sendCommitBatch(changedFiles, allNodes, allEdges, tags, deferIndex, protectedTypes);
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Internal helper: send a commitBatch with chunking for large payloads.
|
|
947
|
-
* Used by both commitBatch() and BatchHandle.commit().
|
|
948
|
-
* @internal
|
|
949
|
-
*/
|
|
950
|
-
async _sendCommitBatch(changedFiles, allNodes, allEdges, tags, deferIndex, protectedTypes) {
|
|
951
|
-
// Chunk large batches to stay under server's 100MB message limit.
|
|
952
|
-
// First chunk includes changedFiles (triggers old data deletion),
|
|
953
|
-
// subsequent chunks use empty changedFiles (additive only).
|
|
954
|
-
const CHUNK = 10_000;
|
|
955
|
-
if (allNodes.length <= CHUNK && allEdges.length <= CHUNK) {
|
|
956
|
-
const response = await this._send('commitBatch', {
|
|
957
|
-
changedFiles, nodes: allNodes, edges: allEdges, tags,
|
|
958
|
-
...(deferIndex ? { deferIndex: true } : {}),
|
|
959
|
-
...(protectedTypes?.length ? { protectedTypes } : {}),
|
|
960
|
-
});
|
|
961
|
-
return response.delta;
|
|
962
|
-
}
|
|
963
|
-
const merged = {
|
|
964
|
-
changedFiles,
|
|
965
|
-
nodesAdded: 0, nodesRemoved: 0,
|
|
966
|
-
edgesAdded: 0, edgesRemoved: 0,
|
|
967
|
-
changedNodeTypes: [], changedEdgeTypes: [],
|
|
968
|
-
};
|
|
969
|
-
const nodeTypes = new Set();
|
|
970
|
-
const edgeTypes = new Set();
|
|
971
|
-
const maxI = Math.max(Math.ceil(allNodes.length / CHUNK), Math.ceil(allEdges.length / CHUNK), 1);
|
|
972
|
-
for (let i = 0; i < maxI; i++) {
|
|
973
|
-
const nodes = allNodes.slice(i * CHUNK, (i + 1) * CHUNK);
|
|
974
|
-
const edges = allEdges.slice(i * CHUNK, (i + 1) * CHUNK);
|
|
975
|
-
const response = await this._send('commitBatch', {
|
|
976
|
-
changedFiles: i === 0 ? changedFiles : [],
|
|
977
|
-
nodes, edges, tags,
|
|
978
|
-
...(deferIndex ? { deferIndex: true } : {}),
|
|
979
|
-
...(i === 0 && protectedTypes?.length ? { protectedTypes } : {}),
|
|
980
|
-
});
|
|
981
|
-
const d = response.delta;
|
|
982
|
-
merged.nodesAdded += d.nodesAdded;
|
|
983
|
-
merged.nodesRemoved += d.nodesRemoved;
|
|
984
|
-
merged.edgesAdded += d.edgesAdded;
|
|
985
|
-
merged.edgesRemoved += d.edgesRemoved;
|
|
986
|
-
for (const t of d.changedNodeTypes)
|
|
987
|
-
nodeTypes.add(t);
|
|
988
|
-
for (const t of d.changedEdgeTypes)
|
|
989
|
-
edgeTypes.add(t);
|
|
990
|
-
}
|
|
991
|
-
merged.changedNodeTypes = [...nodeTypes];
|
|
992
|
-
merged.changedEdgeTypes = [...edgeTypes];
|
|
993
|
-
return merged;
|
|
994
|
-
}
|
|
995
|
-
/**
|
|
996
|
-
* Rebuild all secondary indexes after a series of deferred-index commits.
|
|
997
|
-
* Call this once after bulk loading data with commitBatch(tags, true).
|
|
998
|
-
*/
|
|
999
|
-
async rebuildIndexes() {
|
|
1000
|
-
await this._send('rebuildIndexes', {});
|
|
1001
|
-
}
|
|
1002
346
|
/**
|
|
1003
347
|
* Create an isolated batch handle for concurrent-safe batching.
|
|
1004
|
-
* Each BatchHandle has its own node/edge buffers, avoiding the shared
|
|
1005
|
-
* instance-level _batching state race condition with multiple workers.
|
|
1006
348
|
*/
|
|
1007
349
|
createBatch() {
|
|
1008
350
|
return new BatchHandle(this);
|
|
1009
351
|
}
|
|
1010
|
-
/**
|
|
1011
|
-
* Abort the current batch, discarding all buffered data.
|
|
1012
|
-
*/
|
|
1013
|
-
abortBatch() {
|
|
1014
|
-
this._batching = false;
|
|
1015
|
-
this._batchNodes = [];
|
|
1016
|
-
this._batchEdges = [];
|
|
1017
|
-
this._batchFiles = new Set();
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Check if a batch is currently in progress.
|
|
1021
|
-
*/
|
|
1022
|
-
isBatching() {
|
|
1023
|
-
return this._batching;
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* Find files that depend on the given changed files.
|
|
1027
|
-
* Uses backward reachability to find dependent modules.
|
|
1028
|
-
*
|
|
1029
|
-
* Note: For large result sets, each reachable node requires a separate
|
|
1030
|
-
* getNode RPC. A future server-side optimization could return file paths
|
|
1031
|
-
* directly from the reachability query.
|
|
1032
|
-
*/
|
|
1033
|
-
async findDependentFiles(changedFiles) {
|
|
1034
|
-
const nodeIds = [];
|
|
1035
|
-
for (const file of changedFiles) {
|
|
1036
|
-
const ids = await this.findByAttr({ file });
|
|
1037
|
-
nodeIds.push(...ids);
|
|
1038
|
-
}
|
|
1039
|
-
if (nodeIds.length === 0)
|
|
1040
|
-
return [];
|
|
1041
|
-
const reachable = await this.reachability(nodeIds, 2, ['IMPORTS_FROM', 'DEPENDS_ON', 'CALLS'], true);
|
|
1042
|
-
const changedSet = new Set(changedFiles);
|
|
1043
|
-
const files = new Set();
|
|
1044
|
-
for (const id of reachable) {
|
|
1045
|
-
const node = await this.getNode(id);
|
|
1046
|
-
if (node?.file && !changedSet.has(node.file)) {
|
|
1047
|
-
files.add(node.file);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
return [...files];
|
|
1051
|
-
}
|
|
1052
352
|
/**
|
|
1053
353
|
* Unref the socket so it doesn't keep the process alive.
|
|
1054
|
-
*
|
|
1055
|
-
* Call this in test environments to allow process to exit
|
|
1056
|
-
* even if connections remain open.
|
|
1057
354
|
*/
|
|
1058
355
|
unref() {
|
|
1059
356
|
if (this.socket) {
|
|
@@ -1070,26 +367,9 @@ export class RFDBClient extends EventEmitter {
|
|
|
1070
367
|
this.connected = false;
|
|
1071
368
|
}
|
|
1072
369
|
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Shutdown the server
|
|
1075
|
-
*/
|
|
1076
|
-
async shutdown() {
|
|
1077
|
-
try {
|
|
1078
|
-
await this._send('shutdown');
|
|
1079
|
-
}
|
|
1080
|
-
catch {
|
|
1081
|
-
// Expected - server closes connection
|
|
1082
|
-
}
|
|
1083
|
-
await this.close();
|
|
1084
|
-
}
|
|
1085
370
|
}
|
|
1086
371
|
/**
|
|
1087
372
|
* Isolated batch handle for concurrent-safe batching (REG-487).
|
|
1088
|
-
*
|
|
1089
|
-
* Each BatchHandle maintains its own node/edge/file buffers, completely
|
|
1090
|
-
* independent of the RFDBClient's instance-level _batching state.
|
|
1091
|
-
* Multiple workers can each create their own BatchHandle and commit
|
|
1092
|
-
* independently without race conditions.
|
|
1093
373
|
*/
|
|
1094
374
|
export class BatchHandle {
|
|
1095
375
|
client;
|