• 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

94.28
/src/managers/EventManager.ts
1
import {Observable} from "@babylonjs/core";
3✔
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 {
3✔
30
    // Observables for different event types
31
    private graphObservable = new Observable<GraphEvent>();
939✔
32
    private nodeObservable = new Observable<NodeEvent>();
939✔
33
    private edgeObservable = new Observable<EdgeEvent>();
939✔
34

35
    // Expose for testing and advanced usage
36
    get onGraphEvent(): Observable<GraphEvent> {
939✔
37
        return this.graphObservable;
939✔
38
    }
939✔
39

40
    get onGraphError(): Observable<GraphErrorEvent> {
939✔
41
        return this.graphObservable as Observable<GraphErrorEvent>;
1✔
42
    }
1✔
43

44
    // Track observers for cleanup
45
    // Using any here is required due to BabylonJS Observable/Observer type limitations
46
    private observers = new Map<symbol, {
939✔
47
        type: "graph" | "node" | "edge";
48
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
        observable: any;
50
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
        observer: any;
52
    }>();
939✔
53

54
    // Error handling configuration
55
    private errorRetryCount = 3;
939✔
56
    private errorRetryDelay = 1000; // ms
939✔
57

58
    async init(): Promise<void> {
3✔
59
        // EventManager doesn't need async initialization
60
        return Promise.resolve();
759✔
61
    }
759✔
62

63
    dispose(): void {
3✔
64
        // Clear all observers
65
        for (const {observable, observer} of this.observers.values()) {
616✔
66
            observable.remove(observer);
1,802✔
67
        }
1,802✔
68
        this.observers.clear();
616✔
69

70
        // Clear observables
71
        this.graphObservable.clear();
616✔
72
        this.nodeObservable.clear();
616✔
73
        this.edgeObservable.clear();
616✔
74
    }
616✔
75

76
    // Graph Events
77

78
    emitGraphSettled(graph: Graph): void {
3✔
79
        const event: GraphSettledEvent = {
485✔
80
            type: "graph-settled",
485✔
81
            graph,
485✔
82
        };
485✔
83
        this.graphObservable.notifyObservers(event);
485✔
84
    }
485✔
85

86
    emitGraphError(
3✔
87
        graph: Graph | GraphContext | null,
32✔
88
        error: Error,
32✔
89
        context: GraphErrorEvent["context"],
32✔
90
        details?: Record<string, unknown>,
32✔
91
    ): void {
32✔
92
        const event: GraphErrorEvent = {
32✔
93
            type: "error",
32✔
94
            graph: graph as Graph | null,
32✔
95
            error,
32✔
96
            context,
32✔
97
            details,
32✔
98
        };
32✔
99
        this.graphObservable.notifyObservers(event);
32✔
100
    }
32✔
101

102
    emitGraphDataLoaded(
3✔
103
        graph: Graph | GraphContext,
113✔
104
        chunksLoaded: number,
113✔
105
        dataSourceType: string,
113✔
106
    ): void {
113✔
107
        const event: GraphDataLoadedEvent = {
113✔
108
            type: "data-loaded",
113✔
109
            graph: graph as Graph,
113✔
110
            details: {
113✔
111
                chunksLoaded,
113✔
112
                dataSourceType,
113✔
113
            },
113✔
114
        };
113✔
115
        this.graphObservable.notifyObservers(event);
113✔
116
    }
113✔
117

118
    emitDataAdded(
3✔
119
        dataType: "nodes" | "edges",
13,252✔
120
        count: number,
13,252✔
121
        shouldStartLayout: boolean,
13,252✔
122
        shouldZoomToFit: boolean,
13,252✔
123
    ): void {
13,252✔
124
        const event: GraphDataAddedEvent = {
13,252✔
125
            type: "data-added",
13,252✔
126
            dataType,
13,252✔
127
            count,
13,252✔
128
            shouldStartLayout,
13,252✔
129
            shouldZoomToFit,
13,252✔
130
        };
13,252✔
131
        this.graphObservable.notifyObservers(event);
13,252✔
132
    }
13,252✔
133

134
    emitLayoutInitialized(
3✔
135
        layoutType: string,
2,014✔
136
        shouldZoomToFit: boolean,
2,014✔
137
    ): void {
2,014✔
138
        const event: GraphLayoutInitializedEvent = {
2,014✔
139
            type: "layout-initialized",
2,014✔
140
            layoutType,
2,014✔
141
            shouldZoomToFit,
2,014✔
142
        };
2,014✔
143
        this.graphObservable.notifyObservers(event);
2,014✔
144
    }
2,014✔
145

146
    // Generic graph event emitter for internal events
147
    emitGraphEvent(type: string, data: Record<string, unknown>): void {
3✔
148
        const event = {type, ... data} as GraphGenericEvent;
197,482✔
149
        this.graphObservable.notifyObservers(event);
197,482✔
150
    }
197,482✔
151

152
    // Data Loading Events
153

154
    emitDataLoadingProgress(
3✔
155
        format: string,
112✔
156
        bytesProcessed: number,
112✔
157
        totalBytes: number | undefined,
112✔
158
        nodesLoaded: number,
112✔
159
        edgesLoaded: number,
112✔
160
        chunksProcessed: number,
112✔
161
    ): void {
112✔
162
        const event: DataLoadingProgressEvent = {
112✔
163
            type: "data-loading-progress",
112✔
164
            format,
112✔
165
            bytesProcessed,
112✔
166
            totalBytes,
112✔
167
            percentage: totalBytes ? (bytesProcessed / totalBytes) * 100 : undefined,
112✔
168
            nodesLoaded,
112✔
169
            edgesLoaded,
112✔
170
            chunksProcessed,
112✔
171
        };
112✔
172
        this.graphObservable.notifyObservers(event);
112✔
173
    }
112✔
174

175
    emitDataLoadingError(
3✔
176
        error: Error,
1✔
177
        context: DataLoadingErrorEvent["context"],
1✔
178
        format: string | undefined,
1✔
179
        details: {
1✔
180
            line?: number;
181
            nodeId?: unknown;
182
            edgeId?: string;
183
            canContinue: boolean;
184
        },
185
    ): void {
1✔
186
        const event: DataLoadingErrorEvent = {
1✔
187
            type: "data-loading-error",
1✔
188
            error,
1✔
189
            context,
1✔
190
            format,
1✔
191
            ... details,
1✔
192
        };
1✔
193
        this.graphObservable.notifyObservers(event);
1✔
194
    }
1✔
195

196
    emitDataLoadingErrorSummary(
3✔
197
        format: string,
×
198
        totalErrors: number,
×
199
        message: string,
×
200
        detailedReport: string,
×
201
        primaryCategory?: string,
×
202
        suggestion?: string,
×
203
    ): void {
×
204
        const event: DataLoadingErrorSummaryEvent = {
×
205
            type: "data-loading-error-summary",
×
206
            format,
×
207
            totalErrors,
×
208
            primaryCategory,
×
209
            message,
×
210
            suggestion,
×
211
            detailedReport,
×
212
        };
×
213
        this.graphObservable.notifyObservers(event);
×
214
    }
×
215

216
    emitDataLoadingComplete(
3✔
217
        format: string,
112✔
218
        nodesLoaded: number,
112✔
219
        edgesLoaded: number,
112✔
220
        duration: number,
112✔
221
        errors: number,
112✔
222
        warnings: number,
112✔
223
        success: boolean,
112✔
224
    ): void {
112✔
225
        const event: DataLoadingCompleteEvent = {
112✔
226
            type: "data-loading-complete",
112✔
227
            format,
112✔
228
            nodesLoaded,
112✔
229
            edgesLoaded,
112✔
230
            duration,
112✔
231
            errors,
112✔
232
            warnings,
112✔
233
            success,
112✔
234
        };
112✔
235
        this.graphObservable.notifyObservers(event);
112✔
236
    }
112✔
237

238
    // Selection Events
239

240
    emitSelectionChanged(
3✔
241
        previousNode: SelectionChangedEvent["previousNode"],
24✔
242
        currentNode: SelectionChangedEvent["currentNode"],
24✔
243
    ): void {
24✔
244
        const event: SelectionChangedEvent = {
24✔
245
            type: "selection-changed",
24✔
246
            previousNode,
24✔
247
            currentNode,
24✔
248
            previousNodeId: previousNode?.id ?? null,
24✔
249
            currentNodeId: currentNode?.id ?? null,
24✔
250
        };
24✔
251
        this.graphObservable.notifyObservers(event);
24✔
252
    }
24✔
253

254
    // Node Events
255

256
    emitNodeEvent(type: NodeEvent["type"], eventData: Omit<NodeEvent, "type">): void {
3✔
257
        const event = {type, ... eventData} as NodeEvent;
5,522✔
258
        this.nodeObservable.notifyObservers(event);
5,522✔
259
    }
5,522✔
260

261
    // Edge Events
262

263
    emitEdgeEvent(type: EdgeEvent["type"], eventData: Omit<EdgeEvent, "type">): void {
3✔
264
        const event = {type, ... eventData} as EdgeEvent;
8,897✔
265
        this.edgeObservable.notifyObservers(event);
8,897✔
266
    }
8,897✔
267

268
    // Event Listeners
269

270
    /**
271
     * Add a listener for a specific event type
272
     * Returns a symbol that can be used to remove the listener
273
     */
274
    addListener(type: EventType, callback: EventCallbackType): symbol {
3✔
275
        const id = Symbol("event-listener");
2,610✔
276

277
        switch (type) {
2,610✔
278
            case "graph-settled":
2,610✔
279
            case "error":
2,610✔
280
            case "data-loaded":
2,610✔
281
            case "data-added":
2,610✔
282
            case "layout-initialized":
2,610✔
283
            case "skybox-loaded":
2,610✔
284
            case "operation-queue-active":
2,610✔
285
            case "operation-queue-idle":
2,610✔
286
            case "operation-batch-complete":
2,610✔
287
            case "operation-start":
2,610✔
288
            case "operation-complete":
2,610✔
289
            case "operation-progress":
2,610✔
290
            case "operation-obsoleted":
2,610✔
291
            case "animation-progress":
2,610✔
292
            case "animation-cancelled":
2,610✔
293
            case "screenshot-enhancing":
2,610✔
294
            case "screenshot-ready":
2,610✔
295
            case "style-changed":
2,610✔
296
            case "camera-state-changed":
2,610✔
297
            case "data-loading-progress":
2,610✔
298
            case "data-loading-error":
2,610✔
299
            case "data-loading-error-summary":
2,610✔
300
            case "data-loading-complete":
2,610✔
301
            case "selection-changed": {
2,610✔
302
                const observer = this.graphObservable.add((event) => {
2,602✔
303
                    if (event.type === type) {
631,320✔
304
                        callback(event);
16,854✔
305
                    }
16,854✔
306
                });
2,602✔
307
                this.observers.set(id, {
2,602✔
308
                    type: "graph",
2,602✔
309
                    observable: this.graphObservable,
2,602✔
310
                    observer,
2,602✔
311
                });
2,602✔
312
                break;
2,602✔
313
            }
2,602✔
314

315
            case "node-update-after":
2,610✔
316
            case "node-update-before":
2,610✔
317
            case "node-add-before": {
2,610✔
318
                const observer = this.nodeObservable.add((event) => {
5✔
319
                    if (event.type === type) {
4✔
320
                        callback(event);
3✔
321
                    }
3✔
322
                });
5✔
323
                this.observers.set(id, {
5✔
324
                    type: "node",
5✔
325
                    observable: this.nodeObservable,
5✔
326
                    observer,
5✔
327
                });
5✔
328
                break;
5✔
329
            }
5✔
330

331
            case "edge-update-after":
2,610✔
332
            case "edge-update-before":
2,610✔
333
            case "edge-add-before": {
2,610✔
334
                const observer = this.edgeObservable.add((event) => {
3✔
335
                    if (event.type === type) {
2✔
336
                        callback(event);
2✔
337
                    }
2✔
338
                });
3✔
339
                this.observers.set(id, {
3✔
340
                    type: "edge",
3✔
341
                    observable: this.edgeObservable,
3✔
342
                    observer,
3✔
343
                });
3✔
344
                break;
3✔
345
            }
3✔
346

347
            default:
2,610!
348
                throw new TypeError(`Unknown event type: ${type}`);
×
349
        }
2,610✔
350

351
        return id;
2,610✔
352
    }
2,610✔
353

354
    /**
355
     * Remove a listener by its ID
356
     */
357
    removeListener(id: symbol): boolean {
3✔
358
        const entry = this.observers.get(id);
34✔
359
        if (!entry) {
34✔
360
            return false;
1✔
361
        }
1✔
362

363
        entry.observable.remove(entry.observer);
33✔
364
        this.observers.delete(id);
33✔
365
        return true;
33✔
366
    }
34✔
367

368
    /**
369
     * Get the total number of registered listeners
370
     */
371
    listenerCount(): number {
3✔
372
        return this.observers.size;
2✔
373
    }
2✔
374

375
    /**
376
     * Add a one-time listener that automatically removes itself after firing
377
     */
378
    once(type: EventType, callback: EventCallbackType): symbol {
3✔
379
        const id = this.addListener(type, (event) => {
3✔
380
            callback(event);
2✔
381
            this.removeListener(id);
2✔
382
        });
3✔
383
        return id;
3✔
384
    }
3✔
385

386
    /**
387
     * Wait for a specific event to occur
388
     * Returns a promise that resolves with the event
389
     */
390
    waitFor(type: EventType, timeout?: number): Promise<GraphEvent | NodeEvent | EdgeEvent | import("../events").AiEvent> {
3✔
391
        return new Promise((resolve, reject) => {
2✔
392
            const timeoutId = timeout ? setTimeout(() => {
2✔
393
                this.removeListener(listenerId);
1✔
394
                reject(new Error(`Timeout waiting for event: ${type}`));
1✔
395
            }, timeout) : undefined;
2!
396

397
            const listenerId = this.once(type, (event) => {
2✔
398
                if (timeoutId) {
1✔
399
                    clearTimeout(timeoutId);
1✔
400
                }
1✔
401

402
                resolve(event);
1✔
403
            });
2✔
404
        });
2✔
405
    }
2✔
406

407
    // Error Handling with Retry
408

409
    /**
410
     * Execute an async operation with automatic retry on failure
411
     * Emits error events for each failure
412
     */
413
    async withRetry<T>(
3✔
414
        operation: () => Promise<T>,
2✔
415
        context: GraphErrorEvent["context"],
2✔
416
        graph: Graph | GraphContext | null,
2✔
417
        details?: Record<string, unknown>,
2✔
418
    ): Promise<T> {
2✔
419
        let lastError: Error | undefined;
2✔
420

421
        for (let attempt = 0; attempt < this.errorRetryCount; attempt++) {
2✔
422
            try {
5✔
423
                return await operation();
5✔
424
            } catch (error) {
5✔
425
                lastError = error instanceof Error ? error : new Error(String(error));
4!
426

427
                // Emit error event for this attempt
428
                this.emitGraphError(graph, lastError, context, {
4✔
429
                    ... details,
4✔
430
                    attempt: attempt + 1,
4✔
431
                    maxAttempts: this.errorRetryCount,
4✔
432
                });
4✔
433

434
                // Don't delay after the last attempt
435
                if (attempt < this.errorRetryCount - 1) {
4✔
436
                    // Exponential backoff
437
                    const delay = this.errorRetryDelay * Math.pow(2, attempt);
3✔
438
                    await new Promise((resolve) => setTimeout(resolve, delay));
3✔
439
                }
3✔
440
            }
4✔
441
        }
5✔
442

443
        // All attempts failed
444
        throw lastError ?? new Error("Operation failed with no recorded error");
2!
445
    }
2✔
446
}
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