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

graphty-org / graphty-monorepo / 20661584252

02 Jan 2026 03:50PM UTC coverage: 77.924% (+7.3%) from 70.62%
20661584252

push

github

apowers313
ci: fix flakey performance test

13438 of 17822 branches covered (75.4%)

Branch coverage included in aggregate %.

41247 of 52355 relevant lines covered (78.78%)

145534.85 hits per line

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

87.89
/graphty-element/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<
1,120✔
55
        symbol,
56
        {
57
            type: "graph" | "node" | "edge";
58
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
            observable: any;
60
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
            observer: any;
62
        }
63
    >();
1,120✔
64

65
    // Error handling configuration
66
    private errorRetryCount = 3;
1,120✔
67
    private errorRetryDelay = 1000; // ms
1,120✔
68

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

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

88
        // Clear observables
89
        this.graphObservable.clear();
804✔
90
        this.nodeObservable.clear();
804✔
91
        this.edgeObservable.clear();
804✔
92
    }
804✔
93

94
    // Graph Events
95

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

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

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

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

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

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

197
    // Data Loading Events
198

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

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

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

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

322
    // Selection Events
323

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

343
    // Node Events
344

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

355
    // Edge Events
356

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

367
    // Event Listeners
368

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

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

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

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

449
            default:
3,155!
450
                throw new TypeError(`Unknown event type: ${type}`);
×
451
        }
3,155✔
452

453
        return id;
3,155✔
454
    }
3,155✔
455

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

467
        entry.observable.remove(entry.observer);
33✔
468
        this.observers.delete(id);
33✔
469
        return true;
33✔
470
    }
34✔
471

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

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

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

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

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

523
    // Error Handling with Retry
524

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

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

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

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

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