• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.62
/src/managers/DataManager.ts
1
import jmespath from "jmespath";
15!
2

3
import type {AdHocData} from "../config";
4
import {DataSource} from "../data/DataSource";
15✔
5
import {Edge, EdgeMap} from "../Edge";
15✔
6
import type {LayoutEngine} from "../layout/LayoutEngine";
7
import {GraphtyLogger, type Logger} from "../logging/GraphtyLogger.js";
15✔
8
import {MeshCache} from "../meshes/MeshCache";
15✔
9
import {Node, NodeIdType} from "../Node";
15✔
10
import type {Styles} from "../Styles";
11
import type {EventManager} from "./EventManager";
12
import type {GraphContext} from "./GraphContext";
13
import type {Manager} from "./interfaces";
14

15
// Type guards for layout engines with optional removal methods
16
type LayoutEngineWithRemoveNode = LayoutEngine & {removeNode(node: Node): void};
17
type LayoutEngineWithRemoveEdge = LayoutEngine & {removeEdge(edge: Edge): void};
18

19
function hasRemoveNode(engine: LayoutEngine): engine is LayoutEngineWithRemoveNode {
5✔
20
    return "removeNode" in engine;
5✔
21
}
5✔
22

23
function hasRemoveEdge(engine: LayoutEngine): engine is LayoutEngineWithRemoveEdge {
2✔
24
    return "removeEdge" in engine;
2✔
25
}
2✔
26

27
/**
28
 * Manages all data operations for nodes and edges
29
 * Handles CRUD operations, caching, and data source loading
30
 */
31
export class DataManager implements Manager {
15✔
32
    // Node and edge collections
33
    nodes = new Map<string | number, Node>();
15✔
34
    edges = new Map<string | number, Edge>();
15✔
35
    nodeCache = new Map<NodeIdType, Node>();
15✔
36
    edgeCache = new EdgeMap();
15✔
37
    private logger: Logger = GraphtyLogger.getLogger(["graphty", "data"]);
15✔
38

39
    // Graph-level algorithm results storage
40
    graphResults?: AdHocData;
41

42
    // Mesh cache for performance
43
    meshCache: MeshCache;
44

45
    // GraphContext for creating nodes and edges
46
    private graphContext: GraphContext | null = null;
15✔
47

48
    // State management flags
49
    private shouldStartLayout = false;
15✔
50
    private shouldZoomToFit = false;
15✔
51

52
    // Buffer for edges added before their nodes exist
53
    private bufferedEdges: {
15✔
54
        edge: Record<string | number, unknown>;
55
        srcIdPath?: string;
56
        dstIdPath?: string;
57
    }[] = [];
15✔
58

59
    /**
60
     * Creates an instance of DataManager
61
     * @param eventManager - Event manager for emitting data events
62
     * @param styles - Styles instance for applying styles to data
63
     */
64
    constructor(
15✔
65
        private eventManager: EventManager,
1,007✔
66
        private styles: Styles,
1,007✔
67
    ) {
1,007✔
68
        this.meshCache = new MeshCache();
1,007✔
69
    }
1,007✔
70

71
    /**
72
     * Update the styles reference when styles change
73
     * @param styles - New styles instance to use
74
     */
75
    updateStyles(styles: Styles): void {
15✔
76
        this.styles = styles;
658✔
77
        // Re-apply styles to all existing nodes and edges
78
        this.applyStylesToExistingNodes();
658✔
79
        this.applyStylesToExistingEdges();
658✔
80
    }
658✔
81

82
    /**
83
     * Apply styles to all existing nodes
84
     */
85
    applyStylesToExistingNodes(): void {
15✔
86
        for (const n of this.nodes.values()) {
750✔
87
            // First, recalculate and apply the base style from the new template
88
            // This handles static style values (color, shape, size, etc.)
89
            const newStyleId = this.styles.getStyleForNode(n.data, n.algorithmResults);
2,456✔
90
            n.updateStyle(newStyleId);
2,456✔
91

92
            // Then run calculated values immediately since node data (including algorithmResults) is already populated
93
            // This sets values in n.styleUpdates (same as changeManager.dataObjects.style)
94
            n.changeManager.loadCalculatedValues(this.styles.getCalculatedStylesForNode(n.data), true);
2,456✔
95
            // Call update() to merge calculated style updates with the base style
96
            // update() checks styleUpdates and creates a new styleId that includes calculated values
97
            n.update();
2,456✔
98
        }
2,456✔
99
    }
750✔
100

101
    /**
102
     * Apply styles to all existing edges
103
     */
104
    applyStylesToExistingEdges(): void {
15✔
105
        for (const e of this.edges.values()) {
750✔
106
            // Combine data and algorithmResults for selector matching and calculated style evaluation
107
            const combinedData = {... e.data, algorithmResults: e.algorithmResults};
3,033✔
108

109
            // First, recalculate and apply the base style from the new template
110
            // This handles static style values (color, width, arrow types, etc.)
111
            const newStyleId = this.styles.getStyleForEdge(combinedData);
3,033✔
112
            e.updateStyle(newStyleId);
3,033✔
113

114
            // Then run calculated values immediately since edge data (including algorithmResults) is already populated
115
            // This sets values in e.styleUpdates (same as changeManager.dataObjects.style)
116
            e.changeManager.loadCalculatedValues(this.styles.getCalculatedStylesForEdge(combinedData), true);
3,033✔
117
            // Call update() to merge calculated style updates with the base style
118
            // update() checks styleUpdates and creates a new styleId that includes calculated values
119
            e.update();
3,033✔
120
        }
3,033✔
121
    }
750✔
122

123
    /**
124
     * Set the GraphContext for creating nodes and edges
125
     * @param context - GraphContext instance to use for node/edge creation
126
     */
127
    setGraphContext(context: GraphContext): void {
15✔
128
        this.graphContext = context;
991✔
129
    }
991✔
130

131
    /**
132
     * Set the layout engine reference for adding nodes/edges
133
     */
134
    private layoutEngine?: LayoutEngine;
135

136
    /**
137
     * Set the layout engine reference for managing node and edge positions
138
     * @param engine - Layout engine instance or undefined to clear
139
     */
140
    setLayoutEngine(engine: LayoutEngine | undefined): void {
15✔
141
        this.layoutEngine = engine;
2,485✔
142
    }
2,485✔
143

144
    /**
145
     * Initializes the data manager
146
     * @returns Promise that resolves when initialization is complete
147
     */
148
    async init(): Promise<void> {
15✔
149
        // DataManager doesn't need async initialization
150
        return Promise.resolve();
918✔
151
    }
918✔
152

153
    /**
154
     * Disposes of the data manager and cleans up all resources
155
     */
156
    dispose(): void {
15✔
157
        // Clear all collections
158
        this.nodes.clear();
778✔
159
        this.edges.clear();
778✔
160
        this.nodeCache.clear();
778✔
161
        this.edgeCache.clear();
778✔
162

163
        // Clear graph-level results
164
        this.graphResults = undefined;
778✔
165

166
        // Clear mesh cache
167
        this.meshCache.clear();
778✔
168
    }
778✔
169

170
    // Node operations
171

172
    /**
173
     * Adds a single node to the graph
174
     * @param node - Node data object
175
     * @param idPath - JMESPath expression to extract node ID from data
176
     */
177
    addNode(node: AdHocData, idPath?: string): void {
15✔
178
        this.addNodes([node], idPath);
295✔
179
    }
295✔
180

181
    /**
182
     * Adds multiple nodes to the graph
183
     * @param nodes - Array of node data objects
184
     * @param idPath - JMESPath expression to extract node ID from data
185
     */
186
    addNodes(nodes: Record<string | number, unknown>[], idPath?: string): void {
15✔
187
        this.logger.debug("Adding nodes", {count: nodes.length});
1,432✔
188

189
        // create path to node ids
190
        const query = idPath ?? this.styles.config.data.knownFields.nodeIdPath;
1,432✔
191

192
        // create nodes
193
        for (const node of nodes) {
1,432✔
194
            const nodeId = jmespath.search(node, query) as NodeIdType;
5,637✔
195

196
            if (this.nodeCache.get(nodeId)) {
5,637!
197
                continue;
97✔
198
            }
97✔
199

200
            const styleId = this.styles.getStyleForNode(node as AdHocData);
5,540✔
201
            if (!this.graphContext) {
5,637!
202
                throw new Error("GraphContext not set. Call setGraphContext before adding nodes.");
×
203
            }
✔
204

205
            const n = new Node(this.graphContext, nodeId, styleId, node as AdHocData, {
5,540✔
206
                pinOnDrag: this.graphContext.getConfig().pinOnDrag,
5,540✔
207
            });
5,540✔
208
            this.nodeCache.set(nodeId, n);
5,540✔
209
            this.nodes.set(nodeId, n);
5,540✔
210

211
            // Add to layout engine if it exists
212
            if (this.layoutEngine) {
5,540✔
213
                this.layoutEngine.addNode(n);
5,540✔
214
            }
5,540✔
215

216
            // Emit node added event
217
            this.eventManager.emitNodeEvent("node-add-before", {
5,540✔
218
                nodeId,
5,540✔
219
                metadata: node,
5,540✔
220
            });
5,540✔
221
        }
5,540✔
222

223
        // Notify that nodes were added
224
        if (nodes.length > 0) {
1,432✔
225
            // Request layout start and zoom to fit
226
            this.shouldStartLayout = true;
1,432✔
227
            this.shouldZoomToFit = true;
1,432✔
228

229
            // Process any buffered edges whose nodes now exist
230
            this.processBufferedEdges();
1,432✔
231

232
            // Emit event to notify graph that data has been added
233
            this.eventManager.emitDataAdded("nodes", nodes.length, true, true);
1,432✔
234
        }
1,432✔
235
    }
1,432✔
236

237
    /**
238
     * Process buffered edges whose nodes now exist
239
     * Called after nodes are added to retry edge creation
240
     */
241
    private processBufferedEdges(): void {
15✔
242
        if (this.bufferedEdges.length === 0) {
1,432✔
243
            return;
1,424✔
244
        }
1,424!
245

246
        // Try to process all buffered edges
247
        const stillBuffered: typeof this.bufferedEdges = [];
8✔
248

249
        for (const {edge, srcIdPath, dstIdPath} of this.bufferedEdges) {
93✔
250
            // get paths
251
            const srcQuery = srcIdPath ?? this.styles.config.data.knownFields.edgeSrcIdPath;
26✔
252
            const dstQuery = dstIdPath ?? this.styles.config.data.knownFields.edgeDstIdPath;
26✔
253

254
            const srcNodeId = jmespath.search(edge, srcQuery) as NodeIdType;
26✔
255
            const dstNodeId = jmespath.search(edge, dstQuery) as NodeIdType;
26✔
256

257
            // Check if both nodes now exist
258
            const srcNode = this.nodeCache.get(srcNodeId);
26✔
259
            const dstNode = this.nodeCache.get(dstNodeId);
26✔
260

261
            if (!srcNode || !dstNode) {
26!
262
                // Nodes still don't exist, keep in buffer
263
                stillBuffered.push({edge, srcIdPath, dstIdPath});
9✔
264
                continue;
9✔
265
            }
9✔
266

267
            // Check if edge already exists
268
            if (this.edgeCache.get(srcNodeId, dstNodeId)) {
25!
269
                continue;
×
270
            }
✔
271

272
            // Create the edge now that both nodes exist
273
            const style = this.styles.getStyleForEdge(edge as AdHocData);
17✔
274
            const opts = {};
17✔
275
            if (!this.graphContext) {
25!
276
                throw new Error("GraphContext not set. Call setGraphContext before adding edges.");
×
277
            }
✔
278

279
            const e = new Edge(this.graphContext, srcNodeId, dstNodeId, style, edge as AdHocData, opts);
17✔
280
            this.edgeCache.set(srcNodeId, dstNodeId, e);
17✔
281
            this.edges.set(e.id, e);
17✔
282

283
            // Add to layout engine if it exists
284
            if (this.layoutEngine) {
17✔
285
                this.layoutEngine.addEdge(e);
17✔
286
            }
17✔
287

288
            // Emit edge added event
289
            this.eventManager.emitEdgeEvent("edge-add-before", {
17✔
290
                srcNodeId,
17✔
291
                dstNodeId,
17✔
292
                metadata: edge,
17✔
293
            });
17✔
294
        }
17✔
295

296
        // Update buffer with edges that still couldn't be processed
297
        this.bufferedEdges = stillBuffered;
8✔
298
    }
1,432✔
299

300
    /**
301
     * Gets a node by its ID
302
     * @param nodeId - Node identifier
303
     * @returns Node instance or undefined if not found
304
     */
305
    getNode(nodeId: NodeIdType): Node | undefined {
15✔
306
        return this.nodes.get(nodeId);
136✔
307
    }
136✔
308

309
    /**
310
     * Removes a node from the graph
311
     * @param nodeId - Node identifier to remove
312
     * @returns True if the node was removed, false if not found
313
     */
314
    removeNode(nodeId: NodeIdType): boolean {
15✔
315
        const node = this.nodes.get(nodeId);
6✔
316
        if (!node) {
6!
317
            return false;
1✔
318
        }
1✔
319

320
        // Remove from collections
321
        this.nodes.delete(nodeId);
5✔
322
        this.nodeCache.delete(nodeId);
5✔
323

324
        // Remove from layout engine
325
        if (this.layoutEngine && hasRemoveNode(this.layoutEngine)) {
6!
326
            this.layoutEngine.removeNode(node);
×
327
        }
✔
328

329
        // TODO: Remove connected edges
330

331
        return true;
5✔
332
    }
6✔
333

334
    // Edge operations
335

336
    /**
337
     * Adds a single edge to the graph
338
     * @param edge - Edge data object
339
     * @param srcIdPath - JMESPath expression to extract source node ID from data
340
     * @param dstIdPath - JMESPath expression to extract destination node ID from data
341
     */
342
    addEdge(edge: AdHocData, srcIdPath?: string, dstIdPath?: string): void {
15✔
343
        this.addEdges([edge], srcIdPath, dstIdPath);
167✔
344
    }
167✔
345

346
    /**
347
     * Adds multiple edges to the graph
348
     * @param edges - Array of edge data objects
349
     * @param srcIdPath - JMESPath expression to extract source node ID from data
350
     * @param dstIdPath - JMESPath expression to extract destination node ID from data
351
     */
352
    addEdges(edges: Record<string | number, unknown>[], srcIdPath?: string, dstIdPath?: string): void {
15✔
353
        this.logger.debug("Adding edges", {count: edges.length});
12,051✔
354

355
        // get paths
356
        const srcQuery = srcIdPath ?? this.styles.config.data.knownFields.edgeSrcIdPath;
12,051✔
357
        const dstQuery = dstIdPath ?? this.styles.config.data.knownFields.edgeDstIdPath;
12,051✔
358

359
        // create edges
360
        for (const edge of edges) {
12,051✔
361
            const srcNodeId = jmespath.search(edge, srcQuery) as NodeIdType;
19,816✔
362
            const dstNodeId = jmespath.search(edge, dstQuery) as NodeIdType;
19,816✔
363

364
            if (this.edgeCache.get(srcNodeId, dstNodeId)) {
19,816!
365
                continue;
51✔
366
            }
51✔
367

368
            // Check if both nodes exist before creating edge
369
            const srcNode = this.nodeCache.get(srcNodeId);
19,765✔
370
            const dstNode = this.nodeCache.get(dstNodeId);
19,765✔
371

372
            if (!srcNode || !dstNode) {
19,816✔
373
                // Buffer this edge to be processed later when nodes exist
374
                this.bufferedEdges.push({edge, srcIdPath, dstIdPath});
11,344✔
375
                continue;
11,344✔
376
            }
11,344✔
377

378
            const style = this.styles.getStyleForEdge(edge as AdHocData);
8,421✔
379
            const opts = {};
8,421✔
380
            if (!this.graphContext) {
8,492!
381
                throw new Error("GraphContext not set. Call setGraphContext before adding edges.");
×
382
            }
✔
383

384
            const e = new Edge(this.graphContext, srcNodeId, dstNodeId, style, edge as AdHocData, opts);
8,421✔
385
            this.edgeCache.set(srcNodeId, dstNodeId, e);
8,421✔
386
            this.edges.set(e.id, e);
8,421✔
387

388
            // Add to layout engine if it exists
389
            if (this.layoutEngine) {
8,421✔
390
                this.layoutEngine.addEdge(e);
8,421✔
391
            }
8,421✔
392

393
            // Emit edge added event
394
            this.eventManager.emitEdgeEvent("edge-add-before", {
8,421✔
395
                srcNodeId,
8,421✔
396
                dstNodeId,
8,421✔
397
                metadata: edge,
8,421✔
398
            });
8,421✔
399
        }
8,421✔
400

401
        // Notify that edges were added
402
        if (edges.length > 0) {
12,051✔
403
            // Request layout start
404
            this.shouldStartLayout = true;
12,044✔
405
            // Emit event to notify graph that data has been added
406
            this.eventManager.emitDataAdded("edges", edges.length, true, false);
12,044✔
407
        }
12,044✔
408
    }
12,051✔
409

410
    /**
411
     * Gets an edge by its ID
412
     * @param edgeId - Edge identifier
413
     * @returns Edge instance or undefined if not found
414
     */
415
    getEdge(edgeId: string | number): Edge | undefined {
15✔
416
        return this.edges.get(edgeId);
14✔
417
    }
14✔
418

419
    /**
420
     * Gets an edge between two nodes
421
     * @param srcNodeId - Source node identifier
422
     * @param dstNodeId - Destination node identifier
423
     * @returns Edge instance or undefined if not found
424
     */
425
    getEdgeBetween(srcNodeId: NodeIdType, dstNodeId: NodeIdType): Edge | undefined {
15✔
426
        return this.edgeCache.get(srcNodeId, dstNodeId);
×
427
    }
×
428

429
    /**
430
     * Removes an edge from the graph
431
     * @param edgeId - Edge identifier to remove
432
     * @returns True if the edge was removed, false if not found
433
     */
434
    removeEdge(edgeId: string | number): boolean {
15✔
435
        const edge = this.edges.get(edgeId);
3✔
436
        if (!edge) {
3✔
437
            return false;
1✔
438
        }
1✔
439

440
        // Remove from collections
441
        this.edges.delete(edgeId);
2✔
442
        this.edgeCache.delete(edge.srcNode.id, edge.dstNode.id);
2✔
443

444
        // Remove from layout engine
445
        if (this.layoutEngine && hasRemoveEdge(this.layoutEngine)) {
3!
446
            this.layoutEngine.removeEdge(edge);
×
447
        }
✔
448

449
        return true;
2✔
450
    }
3✔
451

452
    // Data source operations
453

454
    /**
455
     * Loads data from a registered data source
456
     * @param type - Data source type identifier
457
     * @param opts - Options to pass to the data source
458
     */
459
    async addDataFromSource(type: string, opts: object = {}): Promise<void> {
15✔
460
        this.logger.info("Loading data source", {type, options: opts});
111✔
461

462
        const startTime = Date.now();
111✔
463
        let nodesLoaded = 0;
111✔
464
        let edgesLoaded = 0;
111✔
465
        let chunksProcessed = 0;
111✔
466

467
        try {
111✔
468
            const source = DataSource.get(type, opts);
111✔
469
            if (!source) {
111!
470
                throw new TypeError(`No data source named: ${type}`);
×
471
            }
×
472

473
            // Get file size for progress tracking (if available)
474
            const fileSize = (opts as {size?: number}).size;
111✔
475

476
            try {
111✔
477
                for await (const chunk of source.getData()) {
111✔
478
                    this.addNodes(chunk.nodes);
111✔
479
                    this.addEdges(chunk.edges);
111✔
480

481
                    nodesLoaded += chunk.nodes.length;
111✔
482
                    edgesLoaded += chunk.edges.length;
111✔
483
                    chunksProcessed++;
111✔
484

485
                    // Emit progress event
486
                    if (this.graphContext) {
111✔
487
                        this.eventManager.emitDataLoadingProgress(
111✔
488
                            type,
111✔
489
                            chunksProcessed * 64 * 1024, // Approximate bytes (chunk size)
111✔
490
                            fileSize,
111✔
491
                            nodesLoaded,
111✔
492
                            edgesLoaded,
111✔
493
                            chunksProcessed,
111✔
494
                        );
111✔
495
                    }
111✔
496
                }
111✔
497

498
                // Emit error summary if there were errors
499
                if (this.graphContext) {
111✔
500
                    const errorAggregator = source.getErrorAggregator();
111✔
501
                    if (errorAggregator.getErrorCount() > 0) {
111!
502
                        const summary = errorAggregator.getSummary();
×
503
                        this.eventManager.emitDataLoadingErrorSummary(
×
504
                            type,
×
505
                            summary.totalErrors,
×
506
                            summary.message,
×
507
                            errorAggregator.getDetailedReport(),
×
508
                            summary.primaryCategory,
×
509
                            summary.suggestion,
×
510
                        );
×
511
                    }
×
512
                }
111✔
513

514
                // Emit completion event
515
                const duration = Date.now() - startTime;
111✔
516
                const errorCount = source.getErrorAggregator().getErrorCount();
111✔
517

518
                this.logger.info("Data source loading complete", {
111✔
519
                    nodesLoaded,
111✔
520
                    edgesLoaded,
111✔
521
                    duration,
111✔
522
                    chunks: chunksProcessed,
111✔
523
                    errors: errorCount,
111✔
524
                });
111✔
525

526
                if (this.graphContext) {
111✔
527
                    this.eventManager.emitDataLoadingComplete(
111✔
528
                        type,
111✔
529
                        nodesLoaded,
111✔
530
                        edgesLoaded,
111✔
531
                        duration,
111✔
532
                        errorCount,
111✔
533
                        0, // warnings
111✔
534
                        true,
111✔
535
                    );
111✔
536
                }
111✔
537

538
                // Keep existing data-loaded event for backward compatibility
539
                if (this.graphContext) {
111✔
540
                    this.eventManager.emitGraphDataLoaded(this.graphContext, chunksProcessed, type);
111✔
541
                }
111✔
542
            } catch (error) {
111!
543
                // Log the error
544
                this.logger.error(
×
545
                    "Data source loading failed",
×
546
                    error instanceof Error ? error : new Error(String(error)),
×
547
                    {
×
548
                        type,
×
549
                        chunksProcessed,
×
550
                        nodesLoaded,
×
551
                        edgesLoaded,
×
552
                    },
×
553
                );
×
554

555
                // Emit error event
556
                if (this.graphContext) {
×
557
                    this.eventManager.emitDataLoadingError(
×
558
                        error instanceof Error ? error : new Error(String(error)),
×
559
                        "parsing",
×
560
                        type,
×
561
                        {canContinue: false},
×
562
                    );
×
563

564
                    // Keep existing error event for backward compatibility
565
                    this.eventManager.emitGraphError(
×
566
                        this.graphContext,
×
567
                        error instanceof Error ? error : new Error(String(error)),
×
568
                        "data-loading",
×
569
                        {chunksLoaded: chunksProcessed, dataSourceType: type},
×
570
                    );
×
571
                }
×
572

573
                throw new Error(`Failed to load data from source '${type}' after ${chunksProcessed} chunks: ${error instanceof Error ? error.message : String(error)}`);
×
574
            }
×
575
        } catch (error) {
111!
576
            // Re-throw if already a processed error
577
            if (error instanceof Error && error.message.includes("Failed to load data")) {
×
578
                throw error;
×
579
            }
×
580

581
            // Otherwise wrap and throw
582
            throw new Error(`Error initializing data source '${type}': ${error instanceof Error ? error.message : String(error)}`);
×
583
        }
×
584
    }
111✔
585

586
    // Utility methods
587

588
    /**
589
     * Clear all data
590
     */
591
    clear(): void {
15✔
592
        // Remove all nodes and edges
593
        this.nodes.clear();
×
594
        this.edges.clear();
×
595
        this.nodeCache.clear();
×
596
        this.edgeCache.clear();
×
597

598
        // Clear graph-level results
599
        this.graphResults = undefined;
×
600

601
        // Clear mesh cache
602
        this.meshCache.clear();
×
603

604
        // TODO: Notify layout engine to clear
605
    }
×
606

607
    /**
608
     * Start label animations for all nodes
609
     * Called when layout has settled
610
     */
611
    startLabelAnimations(): void {
15✔
612
        for (const node of this.nodes.values()) {
×
613
            node.label?.startAnimation();
×
614
        }
×
615
    }
×
616

617
    /**
618
     * Get statistics about the data
619
     * @returns Object containing node count, edge count, and cached mesh count
620
     */
621
    getStats(): {
15✔
622
        nodeCount: number;
623
        edgeCount: number;
624
        cachedMeshes: number;
625
    } {
×
626
        return {
×
627
            nodeCount: this.nodes.size,
×
628
            edgeCount: this.edges.size,
×
629
            cachedMeshes: this.meshCache.size(),
×
630
        };
×
631
    }
×
632
}
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc