• 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

88.11
/src/managers/EventManager.ts
1
import {Observable} from "@babylonjs/core";
15✔
2

3
import type {
4
    DataLoadingCompleteEvent,
5
    DataLoadingErrorEvent,
6
    DataLoadingErrorSummaryEvent,
7
    DataLoadingProgressEvent,
8
    EdgeEvent,
9
    EventCallbackType,
10
    EventType,
11
    GraphDataAddedEvent,
12
    GraphDataLoadedEvent,
13
    GraphErrorEvent,
14
    GraphEvent,
15
    GraphGenericEvent,
16
    GraphLayoutInitializedEvent,
17
    GraphSettledEvent,
18
    NodeEvent,
19
    SelectionChangedEvent,
20
} from "../events";
21
import type {Graph} from "../Graph";
22
import type {GraphContext} from "./GraphContext";
23
import type {Manager} from "./interfaces";
24

25
/**
26
 * Centralized event management for the Graph system
27
 * Handles all graph, node, and edge events with type safety
28
 */
29
export class EventManager implements Manager {
15✔
30
    // Observables for different event types
31
    private graphObservable = new Observable<GraphEvent>();
1,120✔
32
    private nodeObservable = new Observable<NodeEvent>();
1,120✔
33
    private edgeObservable = new Observable<EdgeEvent>();
1,120✔
34

35
    // Expose for testing and advanced usage
36
    /**
37
     * Gets the graph event observable for direct subscription
38
     * @returns Observable for graph events
39
     */
40
    get onGraphEvent(): Observable<GraphEvent> {
1,120✔
41
        return this.graphObservable;
1,120✔
42
    }
1,120✔
43

44
    /**
45
     * Gets the graph error observable for direct subscription
46
     * @returns Observable for graph error events
47
     */
48
    get onGraphError(): Observable<GraphErrorEvent> {
1,120✔
49
        return this.graphObservable as Observable<GraphErrorEvent>;
1✔
50
    }
1✔
51

52
    // Track observers for cleanup
53
    // Using any here is required due to BabylonJS Observable/Observer type limitations
54
    private observers = new Map<symbol, {
1,120✔
55
        type: "graph" | "node" | "edge";
56
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
        observable: any;
58
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
        observer: any;
60
    }>();
1,120✔
61

62
    // Error handling configuration
63
    private errorRetryCount = 3;
1,120✔
64
    private errorRetryDelay = 1000; // ms
1,120✔
65

66
    /**
67
     * Initializes the event manager
68
     * @returns Promise that resolves when initialization is complete
69
     */
70
    async init(): Promise<void> {
15✔
71
        // EventManager doesn't need async initialization
72
        return Promise.resolve();
940✔
73
    }
940✔
74

75
    /**
76
     * Disposes of the event manager and cleans up all resources
77
     */
78
    dispose(): void {
15✔
79
        // Clear all observers
80
        for (const {observable, observer} of this.observers.values()) {
804✔
81
            observable.remove(observer);
2,367✔
82
        }
2,367✔
83
        this.observers.clear();
804✔
84

85
        // Clear observables
86
        this.graphObservable.clear();
804✔
87
        this.nodeObservable.clear();
804✔
88
        this.edgeObservable.clear();
804✔
89
    }
804✔
90

91
    // Graph Events
92

93
    /**
94
     * Emits a graph settled event when the graph layout has stabilized
95
     * @param graph - Graph instance that has settled
96
     */
97
    emitGraphSettled(graph: Graph): void {
15✔
98
        const event: GraphSettledEvent = {
657✔
99
            type: "graph-settled",
657✔
100
            graph,
657✔
101
        };
657✔
102
        this.graphObservable.notifyObservers(event);
657✔
103
    }
657✔
104

105
    /**
106
     * Emits a graph error event
107
     * @param graph - Graph or GraphContext instance where the error occurred
108
     * @param error - Error object
109
     * @param context - Error context category
110
     * @param details - Additional error details
111
     */
112
    emitGraphError(
15✔
113
        graph: Graph | GraphContext | null,
32✔
114
        error: Error,
32✔
115
        context: GraphErrorEvent["context"],
32✔
116
        details?: Record<string, unknown>,
32✔
117
    ): void {
32✔
118
        const event: GraphErrorEvent = {
32✔
119
            type: "error",
32✔
120
            graph: graph as Graph | null,
32✔
121
            error,
32✔
122
            context,
32✔
123
            details,
32✔
124
        };
32✔
125
        this.graphObservable.notifyObservers(event);
32✔
126
    }
32✔
127

128
    /**
129
     * Emits a data loaded event when data has been loaded from a source
130
     * @param graph - Graph or GraphContext instance
131
     * @param chunksLoaded - Number of data chunks loaded
132
     * @param dataSourceType - Type of data source used
133
     */
134
    emitGraphDataLoaded(
15✔
135
        graph: Graph | GraphContext,
113✔
136
        chunksLoaded: number,
113✔
137
        dataSourceType: string,
113✔
138
    ): void {
113✔
139
        const event: GraphDataLoadedEvent = {
113✔
140
            type: "data-loaded",
113✔
141
            graph: graph as Graph,
113✔
142
            details: {
113✔
143
                chunksLoaded,
113✔
144
                dataSourceType,
113✔
145
            },
113✔
146
        };
113✔
147
        this.graphObservable.notifyObservers(event);
113✔
148
    }
113✔
149

150
    /**
151
     * Emits a data added event when nodes or edges are added
152
     * @param dataType - Type of data added (nodes or edges)
153
     * @param count - Number of items added
154
     * @param shouldStartLayout - Whether layout should be started
155
     * @param shouldZoomToFit - Whether to zoom to fit the data
156
     */
157
    emitDataAdded(
15✔
158
        dataType: "nodes" | "edges",
13,478✔
159
        count: number,
13,478✔
160
        shouldStartLayout: boolean,
13,478✔
161
        shouldZoomToFit: boolean,
13,478✔
162
    ): void {
13,478✔
163
        const event: GraphDataAddedEvent = {
13,478✔
164
            type: "data-added",
13,478✔
165
            dataType,
13,478✔
166
            count,
13,478✔
167
            shouldStartLayout,
13,478✔
168
            shouldZoomToFit,
13,478✔
169
        };
13,478✔
170
        this.graphObservable.notifyObservers(event);
13,478✔
171
    }
13,478✔
172

173
    /**
174
     * Emits a layout initialized event when a layout is ready
175
     * @param layoutType - Type of layout that was initialized
176
     * @param shouldZoomToFit - Whether to zoom to fit after initialization
177
     */
178
    emitLayoutInitialized(
15✔
179
        layoutType: string,
2,487✔
180
        shouldZoomToFit: boolean,
2,487✔
181
    ): void {
2,487✔
182
        const event: GraphLayoutInitializedEvent = {
2,487✔
183
            type: "layout-initialized",
2,487✔
184
            layoutType,
2,487✔
185
            shouldZoomToFit,
2,487✔
186
        };
2,487✔
187
        this.graphObservable.notifyObservers(event);
2,487✔
188
    }
2,487✔
189

190
    // Generic graph event emitter for internal events
191
    /**
192
     * Emits a generic graph event for custom internal events
193
     * @param type - Event type identifier
194
     * @param data - Event data payload
195
     */
196
    emitGraphEvent(type: string, data: Record<string, unknown>): void {
15✔
197
        const event = {type, ... data} as GraphGenericEvent;
208,715✔
198
        this.graphObservable.notifyObservers(event);
208,715✔
199
    }
208,715✔
200

201
    // Data Loading Events
202

203
    /**
204
     * Emits a data loading progress event during data import
205
     * @param format - Data format being loaded
206
     * @param bytesProcessed - Number of bytes processed so far
207
     * @param totalBytes - Total bytes to process (if known)
208
     * @param nodesLoaded - Number of nodes loaded so far
209
     * @param edgesLoaded - Number of edges loaded so far
210
     * @param chunksProcessed - Number of data chunks processed
211
     */
212
    emitDataLoadingProgress(
15✔
213
        format: string,
112✔
214
        bytesProcessed: number,
112✔
215
        totalBytes: number | undefined,
112✔
216
        nodesLoaded: number,
112✔
217
        edgesLoaded: number,
112✔
218
        chunksProcessed: number,
112✔
219
    ): void {
112✔
220
        const event: DataLoadingProgressEvent = {
112✔
221
            type: "data-loading-progress",
112✔
222
            format,
112✔
223
            bytesProcessed,
112✔
224
            totalBytes,
112✔
225
            percentage: totalBytes ? (bytesProcessed / totalBytes) * 100 : undefined,
112!
226
            nodesLoaded,
112✔
227
            edgesLoaded,
112✔
228
            chunksProcessed,
112✔
229
        };
112✔
230
        this.graphObservable.notifyObservers(event);
112✔
231
    }
112✔
232

233
    /**
234
     * Emits a data loading error event when an error occurs during import
235
     * @param error - Error object
236
     * @param context - Error context category
237
     * @param format - Data format being loaded
238
     * @param details - Error details
239
     * @param details.line - Line number where error occurred
240
     * @param details.nodeId - Node ID related to error
241
     * @param details.edgeId - Edge ID related to error
242
     * @param details.canContinue - Whether loading can continue after this error
243
     */
244
    emitDataLoadingError(
15✔
245
        error: Error,
1✔
246
        context: DataLoadingErrorEvent["context"],
1✔
247
        format: string | undefined,
1✔
248
        details: {
1✔
249
            line?: number;
250
            nodeId?: unknown;
251
            edgeId?: string;
252
            canContinue: boolean;
253
        },
254
    ): void {
1✔
255
        const event: DataLoadingErrorEvent = {
1✔
256
            type: "data-loading-error",
1✔
257
            error,
1✔
258
            context,
1✔
259
            format,
1✔
260
            ... details,
1✔
261
        };
1✔
262
        this.graphObservable.notifyObservers(event);
1✔
263
    }
1✔
264

265
    /**
266
     * Emits a summary of all data loading errors after import completes
267
     * @param format - Data format that was loaded
268
     * @param totalErrors - Total number of errors encountered
269
     * @param message - Summary message describing errors
270
     * @param detailedReport - Detailed error report
271
     * @param primaryCategory - Primary error category
272
     * @param suggestion - Suggested fix for the errors
273
     */
274
    emitDataLoadingErrorSummary(
15✔
275
        format: string,
×
276
        totalErrors: number,
×
277
        message: string,
×
278
        detailedReport: string,
×
279
        primaryCategory?: string,
×
280
        suggestion?: string,
×
281
    ): void {
×
282
        const event: DataLoadingErrorSummaryEvent = {
×
283
            type: "data-loading-error-summary",
×
284
            format,
×
285
            totalErrors,
×
286
            primaryCategory,
×
287
            message,
×
288
            suggestion,
×
289
            detailedReport,
×
290
        };
×
291
        this.graphObservable.notifyObservers(event);
×
292
    }
×
293

294
    /**
295
     * Emits a data loading complete event when import finishes
296
     * @param format - Data format that was loaded
297
     * @param nodesLoaded - Number of nodes loaded
298
     * @param edgesLoaded - Number of edges loaded
299
     * @param duration - Time taken to load in milliseconds
300
     * @param errors - Number of errors encountered
301
     * @param warnings - Number of warnings encountered
302
     * @param success - Whether loading was successful
303
     */
304
    emitDataLoadingComplete(
15✔
305
        format: string,
112✔
306
        nodesLoaded: number,
112✔
307
        edgesLoaded: number,
112✔
308
        duration: number,
112✔
309
        errors: number,
112✔
310
        warnings: number,
112✔
311
        success: boolean,
112✔
312
    ): void {
112✔
313
        const event: DataLoadingCompleteEvent = {
112✔
314
            type: "data-loading-complete",
112✔
315
            format,
112✔
316
            nodesLoaded,
112✔
317
            edgesLoaded,
112✔
318
            duration,
112✔
319
            errors,
112✔
320
            warnings,
112✔
321
            success,
112✔
322
        };
112✔
323
        this.graphObservable.notifyObservers(event);
112✔
324
    }
112✔
325

326
    // Selection Events
327

328
    /**
329
     * Emits a selection changed event when node selection changes
330
     * @param previousNode - Previously selected node (or null)
331
     * @param currentNode - Currently selected node (or null)
332
     */
333
    emitSelectionChanged(
15✔
334
        previousNode: SelectionChangedEvent["previousNode"],
44✔
335
        currentNode: SelectionChangedEvent["currentNode"],
44✔
336
    ): void {
44✔
337
        const event: SelectionChangedEvent = {
44✔
338
            type: "selection-changed",
44✔
339
            previousNode,
44✔
340
            currentNode,
44✔
341
            previousNodeId: previousNode?.id ?? null,
44✔
342
            currentNodeId: currentNode?.id ?? null,
44✔
343
        };
44✔
344
        this.graphObservable.notifyObservers(event);
44✔
345
    }
44✔
346

347
    // Node Events
348

349
    /**
350
     * Emits a node event
351
     * @param type - Node event type
352
     * @param eventData - Event data (excluding type field)
353
     */
354
    emitNodeEvent(type: NodeEvent["type"], eventData: Omit<NodeEvent, "type">): void {
15✔
355
        const event = {type, ... eventData} as NodeEvent;
5,617✔
356
        this.nodeObservable.notifyObservers(event);
5,617✔
357
    }
5,617✔
358

359
    // Edge Events
360

361
    /**
362
     * Emits an edge event
363
     * @param type - Edge event type
364
     * @param eventData - Event data (excluding type field)
365
     */
366
    emitEdgeEvent(type: EdgeEvent["type"], eventData: Omit<EdgeEvent, "type">): void {
15✔
367
        const event = {type, ... eventData} as EdgeEvent;
8,441✔
368
        this.edgeObservable.notifyObservers(event);
8,441✔
369
    }
8,441✔
370

371
    // Event Listeners
372

373
    /**
374
     * Add a listener for a specific event type
375
     * Returns a symbol that can be used to remove the listener
376
     * @param type - Event type to listen for
377
     * @param callback - Callback function to invoke when event occurs
378
     * @returns Symbol ID that can be used to remove the listener
379
     */
380
    addListener(type: EventType, callback: EventCallbackType): symbol {
15✔
381
        const id = Symbol("event-listener");
3,155✔
382

383
        switch (type) {
3,155✔
384
            case "graph-settled":
3,155!
385
            case "error":
3,155!
386
            case "data-loaded":
3,155!
387
            case "data-added":
3,155✔
388
            case "layout-initialized":
3,155✔
389
            case "skybox-loaded":
3,155✔
390
            case "operation-queue-active":
3,155✔
391
            case "operation-queue-idle":
3,155✔
392
            case "operation-batch-complete":
3,155✔
393
            case "operation-start":
3,155✔
394
            case "operation-complete":
3,155✔
395
            case "operation-progress":
3,155✔
396
            case "operation-obsoleted":
3,155✔
397
            case "animation-progress":
3,155✔
398
            case "animation-cancelled":
3,155✔
399
            case "screenshot-enhancing":
3,155✔
400
            case "screenshot-ready":
3,155✔
401
            case "style-changed":
3,155✔
402
            case "camera-state-changed":
3,155✔
403
            case "data-loading-progress":
3,155✔
404
            case "data-loading-error":
3,155✔
405
            case "data-loading-error-summary":
3,155✔
406
            case "data-loading-complete":
3,155✔
407
            case "selection-changed": {
3,155✔
408
                const observer = this.graphObservable.add((event) => {
3,147✔
409
                    if (event.type === type) {
665,681✔
410
                        callback(event);
17,870✔
411
                    }
17,870✔
412
                });
3,147✔
413
                this.observers.set(id, {
3,147✔
414
                    type: "graph",
3,147✔
415
                    observable: this.graphObservable,
3,147✔
416
                    observer,
3,147✔
417
                });
3,147✔
418
                break;
3,147✔
419
            }
3,147✔
420

421
            case "node-update-after":
3,155!
422
            case "node-update-before":
3,155!
423
            case "node-add-before": {
3,155!
424
                const observer = this.nodeObservable.add((event) => {
5✔
425
                    if (event.type === type) {
4✔
426
                        callback(event);
3✔
427
                    }
3✔
428
                });
5✔
429
                this.observers.set(id, {
5✔
430
                    type: "node",
5✔
431
                    observable: this.nodeObservable,
5✔
432
                    observer,
5✔
433
                });
5✔
434
                break;
5✔
435
            }
5✔
436

437
            case "edge-update-after":
3,155!
438
            case "edge-update-before":
3,155!
439
            case "edge-add-before": {
3,155!
440
                const observer = this.edgeObservable.add((event) => {
3✔
441
                    if (event.type === type) {
2✔
442
                        callback(event);
2✔
443
                    }
2✔
444
                });
3✔
445
                this.observers.set(id, {
3✔
446
                    type: "edge",
3✔
447
                    observable: this.edgeObservable,
3✔
448
                    observer,
3✔
449
                });
3✔
450
                break;
3✔
451
            }
3✔
452

453
            default:
3,155!
454
                throw new TypeError(`Unknown event type: ${type}`);
×
455
        }
3,155✔
456

457
        return id;
3,155✔
458
    }
3,155✔
459

460
    /**
461
     * Remove a listener by its ID
462
     * @param id - Symbol ID returned from addListener
463
     * @returns True if listener was removed, false if not found
464
     */
465
    removeListener(id: symbol): boolean {
15✔
466
        const entry = this.observers.get(id);
34✔
467
        if (!entry) {
34!
468
            return false;
1✔
469
        }
1✔
470

471
        entry.observable.remove(entry.observer);
33✔
472
        this.observers.delete(id);
33✔
473
        return true;
33✔
474
    }
34✔
475

476
    /**
477
     * Get the total number of registered listeners
478
     * @returns Number of active listeners
479
     */
480
    listenerCount(): number {
15✔
481
        return this.observers.size;
2✔
482
    }
2✔
483

484
    /**
485
     * Add a one-time listener that automatically removes itself after firing
486
     * @param type - Event type to listen for
487
     * @param callback - Callback function to invoke when event occurs
488
     * @returns Symbol ID that can be used to remove the listener
489
     */
490
    once(type: EventType, callback: EventCallbackType): symbol {
15✔
491
        const id = this.addListener(type, (event) => {
3✔
492
            callback(event);
2✔
493
            this.removeListener(id);
2✔
494
        });
3✔
495
        return id;
3✔
496
    }
3✔
497

498
    /**
499
     * Wait for a specific event to occur
500
     * Returns a promise that resolves with the event
501
     * @param type - Event type to wait for
502
     * @param timeout - Optional timeout in milliseconds
503
     * @returns Promise that resolves with the event or rejects on timeout
504
     */
505
    waitFor(type: EventType, timeout?: number): Promise<GraphEvent | NodeEvent | EdgeEvent | import("../events").AiEvent> {
15✔
506
        return new Promise((resolve, reject) => {
2✔
507
            const timeoutId = timeout ? setTimeout(() => {
2✔
508
                this.removeListener(listenerId);
1✔
509
                reject(new Error(`Timeout waiting for event: ${type}`));
1✔
510
            }, timeout) : undefined;
2!
511

512
            const listenerId = this.once(type, (event) => {
2✔
513
                if (timeoutId) {
1✔
514
                    clearTimeout(timeoutId);
1✔
515
                }
1✔
516

517
                resolve(event);
1✔
518
            });
2✔
519
        });
2✔
520
    }
2✔
521

522
    // Error Handling with Retry
523

524
    /**
525
     * Execute an async operation with automatic retry on failure
526
     * Emits error events for each failure
527
     * @param operation - Async operation to execute
528
     * @param context - Error context category
529
     * @param graph - Graph or GraphContext instance
530
     * @param details - Additional error details
531
     * @returns Promise that resolves with operation result or rejects after all retries fail
532
     */
533
    async withRetry<T>(
15✔
534
        operation: () => Promise<T>,
2✔
535
        context: GraphErrorEvent["context"],
2✔
536
        graph: Graph | GraphContext | null,
2✔
537
        details?: Record<string, unknown>,
2✔
538
    ): Promise<T> {
2✔
539
        let lastError: Error | undefined;
2✔
540

541
        for (let attempt = 0; attempt < this.errorRetryCount; attempt++) {
2✔
542
            try {
5✔
543
                return await operation();
5✔
544
            } catch (error) {
5✔
545
                lastError = error instanceof Error ? error : new Error(String(error));
4!
546

547
                // Emit error event for this attempt
548
                this.emitGraphError(graph, lastError, context, {
4✔
549
                    ... details,
4✔
550
                    attempt: attempt + 1,
4✔
551
                    maxAttempts: this.errorRetryCount,
4✔
552
                });
4✔
553

554
                // Don't delay after the last attempt
555
                if (attempt < this.errorRetryCount - 1) {
4✔
556
                    // Exponential backoff
557
                    const delay = this.errorRetryDelay * Math.pow(2, attempt);
3✔
558
                    await new Promise((resolve) => setTimeout(resolve, delay));
3✔
559
                }
3✔
560
            }
4✔
561
        }
5✔
562

563
        // All attempts failed
564
        throw lastError ?? new Error("Operation failed with no recorded error");
2!
565
    }
2✔
566
}
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