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

graphty-org / graphty-element / 20390753610

20 Dec 2025 06:53AM UTC coverage: 82.423% (-1.2%) from 83.666%
20390753610

push

github

apowers313
Merge branch 'master' of https://github.com/graphty-org/graphty-element

5162 of 6088 branches covered (84.79%)

Branch coverage included in aggregate %.

24775 of 30233 relevant lines covered (81.95%)

6480.4 hits per line

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

78.92
/src/managers/DataManager.ts
1
import jmespath from "jmespath";
3!
2

3
import type {AdHocData} from "../config";
4
import {DataSource} from "../data/DataSource";
3✔
5
import {Edge, EdgeMap} from "../Edge";
3✔
6
import type {LayoutEngine} from "../layout/LayoutEngine";
7
import {GraphtyLogger, type Logger} from "../logging/GraphtyLogger.js";
3✔
8
import {MeshCache} from "../meshes/MeshCache";
3✔
9
import {Node, NodeIdType} from "../Node";
3✔
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 {
3✔
32
    // Node and edge collections
33
    nodes = new Map<string | number, Node>();
3✔
34
    edges = new Map<string | number, Edge>();
3✔
35
    nodeCache = new Map<NodeIdType, Node>();
3✔
36
    edgeCache = new EdgeMap();
3✔
37
    private logger: Logger = GraphtyLogger.getLogger(["graphty", "data"]);
3✔
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;
3✔
47

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

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

59
    constructor(
3✔
60
        private eventManager: EventManager,
826✔
61
        private styles: Styles,
826✔
62
    ) {
826✔
63
        this.meshCache = new MeshCache();
826✔
64
    }
826✔
65

66
    /**
67
     * Update the styles reference when styles change
68
     */
69
    updateStyles(styles: Styles): void {
3✔
70
        this.styles = styles;
525✔
71
        // Re-apply styles to all existing nodes and edges
72
        this.applyStylesToExistingNodes();
525✔
73
        this.applyStylesToExistingEdges();
525✔
74
    }
525✔
75

76
    /**
77
     * Apply styles to all existing nodes
78
     */
79
    applyStylesToExistingNodes(): void {
3✔
80
        for (const n of this.nodes.values()) {
617✔
81
            // First, recalculate and apply the base style from the new template
82
            // This handles static style values (color, shape, size, etc.)
83
            const newStyleId = this.styles.getStyleForNode(n.data, n.algorithmResults);
2,639✔
84
            n.updateStyle(newStyleId);
2,639✔
85

86
            // Then run calculated values immediately since node data (including algorithmResults) is already populated
87
            // This sets values in n.styleUpdates (same as changeManager.dataObjects.style)
88
            n.changeManager.loadCalculatedValues(this.styles.getCalculatedStylesForNode(n.data), true);
2,639✔
89
            // Call update() to merge calculated style updates with the base style
90
            // update() checks styleUpdates and creates a new styleId that includes calculated values
91
            n.update();
2,639✔
92
        }
2,639✔
93
    }
617✔
94

95
    /**
96
     * Apply styles to all existing edges
97
     */
98
    applyStylesToExistingEdges(): void {
3✔
99
        for (const e of this.edges.values()) {
617✔
100
            // Combine data and algorithmResults for selector matching and calculated style evaluation
101
            const combinedData = {... e.data, algorithmResults: e.algorithmResults};
3,620✔
102

103
            // First, recalculate and apply the base style from the new template
104
            // This handles static style values (color, width, arrow types, etc.)
105
            const newStyleId = this.styles.getStyleForEdge(combinedData);
3,620✔
106
            e.updateStyle(newStyleId);
3,620✔
107

108
            // Then run calculated values immediately since edge data (including algorithmResults) is already populated
109
            // This sets values in e.styleUpdates (same as changeManager.dataObjects.style)
110
            e.changeManager.loadCalculatedValues(this.styles.getCalculatedStylesForEdge(combinedData), true);
3,620✔
111
            // Call update() to merge calculated style updates with the base style
112
            // update() checks styleUpdates and creates a new styleId that includes calculated values
113
            e.update();
3,620✔
114
        }
3,620✔
115
    }
617✔
116

117
    /**
118
     * Set the GraphContext for creating nodes and edges
119
     */
120
    setGraphContext(context: GraphContext): void {
3✔
121
        this.graphContext = context;
810✔
122
    }
810✔
123

124
    /**
125
     * Set the layout engine reference for adding nodes/edges
126
     */
127
    private layoutEngine?: LayoutEngine;
128

129
    setLayoutEngine(engine: LayoutEngine | undefined): void {
3✔
130
        this.layoutEngine = engine;
2,012✔
131
    }
2,012✔
132

133
    async init(): Promise<void> {
3✔
134
        // DataManager doesn't need async initialization
135
        return Promise.resolve();
738✔
136
    }
738✔
137

138
    dispose(): void {
3✔
139
        // Clear all collections
140
        this.nodes.clear();
590✔
141
        this.edges.clear();
590✔
142
        this.nodeCache.clear();
590✔
143
        this.edgeCache.clear();
590✔
144

145
        // Clear graph-level results
146
        this.graphResults = undefined;
590✔
147

148
        // Clear mesh cache
149
        this.meshCache.clear();
590✔
150
    }
590✔
151

152
    // Node operations
153

154
    addNode(node: AdHocData, idPath?: string): void {
3✔
155
        this.addNodes([node], idPath);
295✔
156
    }
295✔
157

158
    addNodes(nodes: Record<string | number, unknown>[], idPath?: string): void {
3✔
159
        this.logger.debug("Adding nodes", {count: nodes.length});
1,313✔
160

161
        // create path to node ids
162
        const query = idPath ?? this.styles.config.data.knownFields.nodeIdPath;
1,313✔
163

164
        // create nodes
165
        for (const node of nodes) {
1,313✔
166
            const nodeId = jmespath.search(node, query) as NodeIdType;
5,615✔
167

168
            if (this.nodeCache.get(nodeId)) {
5,615✔
169
                continue;
97✔
170
            }
97✔
171

172
            const styleId = this.styles.getStyleForNode(node as AdHocData);
5,518✔
173
            if (!this.graphContext) {
5,615!
174
                throw new Error("GraphContext not set. Call setGraphContext before adding nodes.");
×
175
            }
✔
176

177
            const n = new Node(this.graphContext, nodeId, styleId, node as AdHocData, {
5,518✔
178
                pinOnDrag: this.graphContext.getConfig().pinOnDrag,
5,518✔
179
            });
5,518✔
180
            this.nodeCache.set(nodeId, n);
5,518✔
181
            this.nodes.set(nodeId, n);
5,518✔
182

183
            // Add to layout engine if it exists
184
            if (this.layoutEngine) {
5,518✔
185
                this.layoutEngine.addNode(n);
5,518✔
186
            }
5,518✔
187

188
            // Emit node added event
189
            this.eventManager.emitNodeEvent("node-add-before", {
5,518✔
190
                nodeId,
5,518✔
191
                metadata: node,
5,518✔
192
            });
5,518✔
193
        }
5,518✔
194

195
        // Notify that nodes were added
196
        if (nodes.length > 0) {
1,313✔
197
            // Request layout start and zoom to fit
198
            this.shouldStartLayout = true;
1,313✔
199
            this.shouldZoomToFit = true;
1,313✔
200

201
            // Process any buffered edges whose nodes now exist
202
            this.processBufferedEdges();
1,313✔
203

204
            // Emit event to notify graph that data has been added
205
            this.eventManager.emitDataAdded("nodes", nodes.length, true, true);
1,313✔
206
        }
1,313✔
207
    }
1,313✔
208

209
    /**
210
     * Process buffered edges whose nodes now exist
211
     * Called after nodes are added to retry edge creation
212
     */
213
    private processBufferedEdges(): void {
3✔
214
        if (this.bufferedEdges.length === 0) {
1,313✔
215
            return;
1,305✔
216
        }
1,305✔
217

218
        // Try to process all buffered edges
219
        const stillBuffered: typeof this.bufferedEdges = [];
8✔
220

221
        for (const {edge, srcIdPath, dstIdPath} of this.bufferedEdges) {
93✔
222
            // get paths
223
            const srcQuery = srcIdPath ?? this.styles.config.data.knownFields.edgeSrcIdPath;
26✔
224
            const dstQuery = dstIdPath ?? this.styles.config.data.knownFields.edgeDstIdPath;
26✔
225

226
            const srcNodeId = jmespath.search(edge, srcQuery) as NodeIdType;
26✔
227
            const dstNodeId = jmespath.search(edge, dstQuery) as NodeIdType;
26✔
228

229
            // Check if both nodes now exist
230
            const srcNode = this.nodeCache.get(srcNodeId);
26✔
231
            const dstNode = this.nodeCache.get(dstNodeId);
26✔
232

233
            if (!srcNode || !dstNode) {
26✔
234
                // Nodes still don't exist, keep in buffer
235
                stillBuffered.push({edge, srcIdPath, dstIdPath});
9✔
236
                continue;
9✔
237
            }
9✔
238

239
            // Check if edge already exists
240
            if (this.edgeCache.get(srcNodeId, dstNodeId)) {
25!
241
                continue;
×
242
            }
✔
243

244
            // Create the edge now that both nodes exist
245
            const style = this.styles.getStyleForEdge(edge as AdHocData);
17✔
246
            const opts = {};
17✔
247
            if (!this.graphContext) {
25!
248
                throw new Error("GraphContext not set. Call setGraphContext before adding edges.");
×
249
            }
✔
250

251
            const e = new Edge(this.graphContext, srcNodeId, dstNodeId, style, edge as AdHocData, opts);
17✔
252
            this.edgeCache.set(srcNodeId, dstNodeId, e);
17✔
253
            this.edges.set(e.id, e);
17✔
254

255
            // Add to layout engine if it exists
256
            if (this.layoutEngine) {
17✔
257
                this.layoutEngine.addEdge(e);
17✔
258
            }
17✔
259

260
            // Emit edge added event
261
            this.eventManager.emitEdgeEvent("edge-add-before", {
17✔
262
                srcNodeId,
17✔
263
                dstNodeId,
17✔
264
                metadata: edge,
17✔
265
            });
17✔
266
        }
17✔
267

268
        // Update buffer with edges that still couldn't be processed
269
        this.bufferedEdges = stillBuffered;
8✔
270
    }
1,313✔
271

272
    getNode(nodeId: NodeIdType): Node | undefined {
3✔
273
        return this.nodes.get(nodeId);
62✔
274
    }
62✔
275

276
    removeNode(nodeId: NodeIdType): boolean {
3✔
277
        const node = this.nodes.get(nodeId);
6✔
278
        if (!node) {
6✔
279
            return false;
1✔
280
        }
1✔
281

282
        // Remove from collections
283
        this.nodes.delete(nodeId);
5✔
284
        this.nodeCache.delete(nodeId);
5✔
285

286
        // Remove from layout engine
287
        if (this.layoutEngine && hasRemoveNode(this.layoutEngine)) {
6!
288
            this.layoutEngine.removeNode(node);
×
289
        }
✔
290

291
        // TODO: Remove connected edges
292

293
        return true;
5✔
294
    }
6✔
295

296
    // Edge operations
297

298
    addEdge(edge: AdHocData, srcIdPath?: string, dstIdPath?: string): void {
3✔
299
        this.addEdges([edge], srcIdPath, dstIdPath);
167✔
300
    }
167✔
301

302
    addEdges(edges: Record<string | number, unknown>[], srcIdPath?: string, dstIdPath?: string): void {
3✔
303
        this.logger.debug("Adding edges", {count: edges.length});
11,944✔
304

305
        // get paths
306
        const srcQuery = srcIdPath ?? this.styles.config.data.knownFields.edgeSrcIdPath;
11,944✔
307
        const dstQuery = dstIdPath ?? this.styles.config.data.knownFields.edgeDstIdPath;
11,944✔
308

309
        // create edges
310
        for (const edge of edges) {
11,944✔
311
            const srcNodeId = jmespath.search(edge, srcQuery) as NodeIdType;
20,280✔
312
            const dstNodeId = jmespath.search(edge, dstQuery) as NodeIdType;
20,280✔
313

314
            if (this.edgeCache.get(srcNodeId, dstNodeId)) {
20,280✔
315
                continue;
59✔
316
            }
59✔
317

318
            // Check if both nodes exist before creating edge
319
            const srcNode = this.nodeCache.get(srcNodeId);
20,221✔
320
            const dstNode = this.nodeCache.get(dstNodeId);
20,221✔
321

322
            if (!srcNode || !dstNode) {
20,280✔
323
                // Buffer this edge to be processed later when nodes exist
324
                this.bufferedEdges.push({edge, srcIdPath, dstIdPath});
11,344✔
325
                continue;
11,344✔
326
            }
11,344✔
327

328
            const style = this.styles.getStyleForEdge(edge as AdHocData);
8,877✔
329
            const opts = {};
8,877✔
330
            if (!this.graphContext) {
8,956!
331
                throw new Error("GraphContext not set. Call setGraphContext before adding edges.");
×
332
            }
✔
333

334
            const e = new Edge(this.graphContext, srcNodeId, dstNodeId, style, edge as AdHocData, opts);
8,877✔
335
            this.edgeCache.set(srcNodeId, dstNodeId, e);
8,877✔
336
            this.edges.set(e.id, e);
8,877✔
337

338
            // Add to layout engine if it exists
339
            if (this.layoutEngine) {
8,877✔
340
                this.layoutEngine.addEdge(e);
8,877✔
341
            }
8,877✔
342

343
            // Emit edge added event
344
            this.eventManager.emitEdgeEvent("edge-add-before", {
8,877✔
345
                srcNodeId,
8,877✔
346
                dstNodeId,
8,877✔
347
                metadata: edge,
8,877✔
348
            });
8,877✔
349
        }
8,877✔
350

351
        // Notify that edges were added
352
        if (edges.length > 0) {
11,944✔
353
            // Request layout start
354
            this.shouldStartLayout = true;
11,937✔
355
            // Emit event to notify graph that data has been added
356
            this.eventManager.emitDataAdded("edges", edges.length, true, false);
11,937✔
357
        }
11,937✔
358
    }
11,944✔
359

360
    getEdge(edgeId: string | number): Edge | undefined {
3✔
361
        return this.edges.get(edgeId);
14✔
362
    }
14✔
363

364
    getEdgeBetween(srcNodeId: NodeIdType, dstNodeId: NodeIdType): Edge | undefined {
3✔
365
        return this.edgeCache.get(srcNodeId, dstNodeId);
×
366
    }
×
367

368
    removeEdge(edgeId: string | number): boolean {
3✔
369
        const edge = this.edges.get(edgeId);
3✔
370
        if (!edge) {
3✔
371
            return false;
1✔
372
        }
1✔
373

374
        // Remove from collections
375
        this.edges.delete(edgeId);
2✔
376
        this.edgeCache.delete(edge.srcNode.id, edge.dstNode.id);
2✔
377

378
        // Remove from layout engine
379
        if (this.layoutEngine && hasRemoveEdge(this.layoutEngine)) {
3!
380
            this.layoutEngine.removeEdge(edge);
×
381
        }
✔
382

383
        return true;
2✔
384
    }
3✔
385

386
    // Data source operations
387

388
    async addDataFromSource(type: string, opts: object = {}): Promise<void> {
3✔
389
        this.logger.info("Loading data source", {type, options: opts});
111✔
390

391
        const startTime = Date.now();
111✔
392
        let nodesLoaded = 0;
111✔
393
        let edgesLoaded = 0;
111✔
394
        let chunksProcessed = 0;
111✔
395

396
        try {
111✔
397
            const source = DataSource.get(type, opts);
111✔
398
            if (!source) {
111!
399
                throw new TypeError(`No data source named: ${type}`);
×
400
            }
×
401

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

405
            try {
111✔
406
                for await (const chunk of source.getData()) {
111✔
407
                    this.addNodes(chunk.nodes);
111✔
408
                    this.addEdges(chunk.edges);
111✔
409

410
                    nodesLoaded += chunk.nodes.length;
111✔
411
                    edgesLoaded += chunk.edges.length;
111✔
412
                    chunksProcessed++;
111✔
413

414
                    // Emit progress event
415
                    if (this.graphContext) {
111✔
416
                        this.eventManager.emitDataLoadingProgress(
111✔
417
                            type,
111✔
418
                            chunksProcessed * 64 * 1024, // Approximate bytes (chunk size)
111✔
419
                            fileSize,
111✔
420
                            nodesLoaded,
111✔
421
                            edgesLoaded,
111✔
422
                            chunksProcessed,
111✔
423
                        );
111✔
424
                    }
111✔
425
                }
111✔
426

427
                // Emit error summary if there were errors
428
                if (this.graphContext) {
111✔
429
                    const errorAggregator = source.getErrorAggregator();
111✔
430
                    if (errorAggregator.getErrorCount() > 0) {
111!
431
                        const summary = errorAggregator.getSummary();
×
432
                        this.eventManager.emitDataLoadingErrorSummary(
×
433
                            type,
×
434
                            summary.totalErrors,
×
435
                            summary.message,
×
436
                            errorAggregator.getDetailedReport(),
×
437
                            summary.primaryCategory,
×
438
                            summary.suggestion,
×
439
                        );
×
440
                    }
×
441
                }
111✔
442

443
                // Emit completion event
444
                const duration = Date.now() - startTime;
111✔
445
                const errorCount = source.getErrorAggregator().getErrorCount();
111✔
446

447
                this.logger.info("Data source loading complete", {
111✔
448
                    nodesLoaded,
111✔
449
                    edgesLoaded,
111✔
450
                    duration,
111✔
451
                    chunks: chunksProcessed,
111✔
452
                    errors: errorCount,
111✔
453
                });
111✔
454

455
                if (this.graphContext) {
111✔
456
                    this.eventManager.emitDataLoadingComplete(
111✔
457
                        type,
111✔
458
                        nodesLoaded,
111✔
459
                        edgesLoaded,
111✔
460
                        duration,
111✔
461
                        errorCount,
111✔
462
                        0, // warnings
111✔
463
                        true,
111✔
464
                    );
111✔
465
                }
111✔
466

467
                // Keep existing data-loaded event for backward compatibility
468
                if (this.graphContext) {
111✔
469
                    this.eventManager.emitGraphDataLoaded(this.graphContext, chunksProcessed, type);
111✔
470
                }
111✔
471
            } catch (error) {
111!
472
                // Log the error
473
                this.logger.error(
×
474
                    "Data source loading failed",
×
475
                    error instanceof Error ? error : new Error(String(error)),
×
476
                    {
×
477
                        type,
×
478
                        chunksProcessed,
×
479
                        nodesLoaded,
×
480
                        edgesLoaded,
×
481
                    },
×
482
                );
×
483

484
                // Emit error event
485
                if (this.graphContext) {
×
486
                    this.eventManager.emitDataLoadingError(
×
487
                        error instanceof Error ? error : new Error(String(error)),
×
488
                        "parsing",
×
489
                        type,
×
490
                        {canContinue: false},
×
491
                    );
×
492

493
                    // Keep existing error event for backward compatibility
494
                    this.eventManager.emitGraphError(
×
495
                        this.graphContext,
×
496
                        error instanceof Error ? error : new Error(String(error)),
×
497
                        "data-loading",
×
498
                        {chunksLoaded: chunksProcessed, dataSourceType: type},
×
499
                    );
×
500
                }
×
501

502
                throw new Error(`Failed to load data from source '${type}' after ${chunksProcessed} chunks: ${error instanceof Error ? error.message : String(error)}`);
×
503
            }
×
504
        } catch (error) {
111!
505
            // Re-throw if already a processed error
506
            if (error instanceof Error && error.message.includes("Failed to load data")) {
×
507
                throw error;
×
508
            }
×
509

510
            // Otherwise wrap and throw
511
            throw new Error(`Error initializing data source '${type}': ${error instanceof Error ? error.message : String(error)}`);
×
512
        }
×
513
    }
111✔
514

515
    // Utility methods
516

517
    /**
518
     * Clear all data
519
     */
520
    clear(): void {
3✔
521
        // Remove all nodes and edges
522
        this.nodes.clear();
×
523
        this.edges.clear();
×
524
        this.nodeCache.clear();
×
525
        this.edgeCache.clear();
×
526

527
        // Clear graph-level results
528
        this.graphResults = undefined;
×
529

530
        // Clear mesh cache
531
        this.meshCache.clear();
×
532

533
        // TODO: Notify layout engine to clear
534
    }
×
535

536
    /**
537
     * Start label animations for all nodes
538
     * Called when layout has settled
539
     */
540
    startLabelAnimations(): void {
3✔
541
        for (const node of this.nodes.values()) {
×
542
            node.label?.startAnimation();
×
543
        }
×
544
    }
×
545

546
    /**
547
     * Get statistics about the data
548
     */
549
    getStats(): {
3✔
550
        nodeCount: number;
551
        edgeCount: number;
552
        cachedMeshes: number;
553
    } {
×
554
        return {
×
555
            nodeCount: this.nodes.size,
×
556
            edgeCount: this.edges.size,
×
557
            cachedMeshes: this.meshCache.size(),
×
558
        };
×
559
    }
×
560
}
3✔
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