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

graphty-org / graphty-element / 18789674490

24 Oct 2025 07:11PM UTC coverage: 82.377% (+0.01%) from 82.365%
18789674490

push

github

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

1259 of 1541 branches covered (81.7%)

Branch coverage included in aggregate %.

6823 of 8270 relevant lines covered (82.5%)

580.52 hits per line

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

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

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

20
/**
21
 * Centralized event management for the Graph system
22
 * Handles all graph, node, and edge events with type safety
23
 */
24
export class EventManager implements Manager {
3✔
25
    // Observables for different event types
26
    private graphObservable = new Observable<GraphEvent>();
223✔
27
    private nodeObservable = new Observable<NodeEvent>();
223✔
28
    private edgeObservable = new Observable<EdgeEvent>();
223✔
29

30
    // Expose for testing and advanced usage
31
    get onGraphEvent(): Observable<GraphEvent> {
223✔
32
        return this.graphObservable;
223✔
33
    }
223✔
34

35
    get onGraphError(): Observable<GraphErrorEvent> {
223✔
36
        return this.graphObservable as Observable<GraphErrorEvent>;
×
37
    }
×
38

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

49
    // Error handling configuration
50
    private errorRetryCount = 3;
223✔
51
    private errorRetryDelay = 1000; // ms
223✔
52

53
    async init(): Promise<void> {
3✔
54
        // EventManager doesn't need async initialization
55
        return Promise.resolve();
192✔
56
    }
192✔
57

58
    dispose(): void {
3✔
59
        // Clear all observers
60
        for (const {observable, observer} of this.observers.values()) {
163✔
61
            observable.remove(observer);
435✔
62
        }
435✔
63
        this.observers.clear();
163✔
64

65
        // Clear observables
66
        this.graphObservable.clear();
163✔
67
        this.nodeObservable.clear();
163✔
68
        this.edgeObservable.clear();
163✔
69
    }
163✔
70

71
    // Graph Events
72

73
    emitGraphSettled(graph: Graph): void {
3✔
74
        const event: GraphSettledEvent = {
111✔
75
            type: "graph-settled",
111✔
76
            graph,
111✔
77
        };
111✔
78
        this.graphObservable.notifyObservers(event);
111✔
79
    }
111✔
80

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

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

113
    emitDataAdded(
3✔
114
        dataType: "nodes" | "edges",
211✔
115
        count: number,
211✔
116
        shouldStartLayout: boolean,
211✔
117
        shouldZoomToFit: boolean,
211✔
118
    ): void {
211✔
119
        const event: GraphDataAddedEvent = {
211✔
120
            type: "data-added",
211✔
121
            dataType,
211✔
122
            count,
211✔
123
            shouldStartLayout,
211✔
124
            shouldZoomToFit,
211✔
125
        };
211✔
126
        this.graphObservable.notifyObservers(event);
211✔
127
    }
211✔
128

129
    emitLayoutInitialized(
3✔
130
        layoutType: string,
263✔
131
        shouldZoomToFit: boolean,
263✔
132
    ): void {
263✔
133
        const event: GraphLayoutInitializedEvent = {
263✔
134
            type: "layout-initialized",
263✔
135
            layoutType,
263✔
136
            shouldZoomToFit,
263✔
137
        };
263✔
138
        this.graphObservable.notifyObservers(event);
263✔
139
    }
263✔
140

141
    // Generic graph event emitter for internal events
142
    emitGraphEvent(type: string, data: Record<string, unknown>): void {
3✔
143
        const event = {type, ... data} as GraphGenericEvent;
3,246✔
144
        this.graphObservable.notifyObservers(event);
3,246✔
145
    }
3,246✔
146

147
    // Node Events
148

149
    emitNodeEvent(type: NodeEvent["type"], eventData: Omit<NodeEvent, "type">): void {
3✔
150
        const event = {type, ... eventData} as NodeEvent;
2,473✔
151
        this.nodeObservable.notifyObservers(event);
2,473✔
152
    }
2,473✔
153

154
    // Edge Events
155

156
    emitEdgeEvent(type: EdgeEvent["type"], eventData: Omit<EdgeEvent, "type">): void {
3✔
157
        const event = {type, ... eventData} as EdgeEvent;
5,952✔
158
        this.edgeObservable.notifyObservers(event);
5,952✔
159
    }
5,952✔
160

161
    // Event Listeners
162

163
    /**
164
     * Add a listener for a specific event type
165
     * Returns a symbol that can be used to remove the listener
166
     */
167
    addListener(type: EventType, callback: EventCallbackType): symbol {
3✔
168
        const id = Symbol("event-listener");
511✔
169

170
        switch (type) {
511✔
171
            case "graph-settled":
511✔
172
            case "error":
511✔
173
            case "data-loaded":
511✔
174
            case "data-added":
511✔
175
            case "layout-initialized":
511✔
176
            case "skybox-loaded":
511✔
177
            case "operation-queue-active":
511✔
178
            case "operation-queue-idle":
511✔
179
            case "operation-batch-complete":
511✔
180
            case "operation-start":
511✔
181
            case "operation-complete":
511✔
182
            case "operation-progress":
511✔
183
            case "operation-obsoleted": {
511✔
184
                const observer = this.graphObservable.add((event) => {
503✔
185
                    if (event.type === type) {
9,927✔
186
                        callback(event);
544✔
187
                    }
544✔
188
                });
503✔
189
                this.observers.set(id, {
503✔
190
                    type: "graph",
503✔
191
                    observable: this.graphObservable,
503✔
192
                    observer,
503✔
193
                });
503✔
194
                break;
503✔
195
            }
503✔
196

197
            case "node-update-after":
511✔
198
            case "node-update-before":
511✔
199
            case "node-add-before": {
511✔
200
                const observer = this.nodeObservable.add((event) => {
5✔
201
                    if (event.type === type) {
4✔
202
                        callback(event);
3✔
203
                    }
3✔
204
                });
5✔
205
                this.observers.set(id, {
5✔
206
                    type: "node",
5✔
207
                    observable: this.nodeObservable,
5✔
208
                    observer,
5✔
209
                });
5✔
210
                break;
5✔
211
            }
5✔
212

213
            case "edge-update-after":
511✔
214
            case "edge-update-before":
511✔
215
            case "edge-add-before": {
511✔
216
                const observer = this.edgeObservable.add((event) => {
3✔
217
                    if (event.type === type) {
2✔
218
                        callback(event);
2✔
219
                    }
2✔
220
                });
3✔
221
                this.observers.set(id, {
3✔
222
                    type: "edge",
3✔
223
                    observable: this.edgeObservable,
3✔
224
                    observer,
3✔
225
                });
3✔
226
                break;
3✔
227
            }
3✔
228

229
            default:
511!
230
                throw new TypeError(`Unknown event type: ${type}`);
×
231
        }
511✔
232

233
        return id;
511✔
234
    }
511✔
235

236
    /**
237
     * Remove a listener by its ID
238
     */
239
    removeListener(id: symbol): boolean {
3✔
240
        const entry = this.observers.get(id);
21✔
241
        if (!entry) {
21✔
242
            return false;
1✔
243
        }
1✔
244

245
        entry.observable.remove(entry.observer);
20✔
246
        this.observers.delete(id);
20✔
247
        return true;
20✔
248
    }
21✔
249

250
    /**
251
     * Add a one-time listener that automatically removes itself after firing
252
     */
253
    once(type: EventType, callback: EventCallbackType): symbol {
3✔
254
        const id = this.addListener(type, (event) => {
3✔
255
            callback(event);
2✔
256
            this.removeListener(id);
2✔
257
        });
3✔
258
        return id;
3✔
259
    }
3✔
260

261
    /**
262
     * Wait for a specific event to occur
263
     * Returns a promise that resolves with the event
264
     */
265
    waitFor(type: EventType, timeout?: number): Promise<GraphEvent | NodeEvent | EdgeEvent> {
3✔
266
        return new Promise((resolve, reject) => {
2✔
267
            const timeoutId = timeout ? setTimeout(() => {
2✔
268
                this.removeListener(listenerId);
1✔
269
                reject(new Error(`Timeout waiting for event: ${type}`));
1✔
270
            }, timeout) : undefined;
2!
271

272
            const listenerId = this.once(type, (event) => {
2✔
273
                if (timeoutId) {
1✔
274
                    clearTimeout(timeoutId);
1✔
275
                }
1✔
276

277
                resolve(event);
1✔
278
            });
2✔
279
        });
2✔
280
    }
2✔
281

282
    // Error Handling with Retry
283

284
    /**
285
     * Execute an async operation with automatic retry on failure
286
     * Emits error events for each failure
287
     */
288
    async withRetry<T>(
3✔
289
        operation: () => Promise<T>,
2✔
290
        context: GraphErrorEvent["context"],
2✔
291
        graph: Graph | GraphContext | null,
2✔
292
        details?: Record<string, unknown>,
2✔
293
    ): Promise<T> {
2✔
294
        let lastError: Error | undefined;
2✔
295

296
        for (let attempt = 0; attempt < this.errorRetryCount; attempt++) {
2✔
297
            try {
5✔
298
                return await operation();
5✔
299
            } catch (error) {
5✔
300
                lastError = error instanceof Error ? error : new Error(String(error));
4!
301

302
                // Emit error event for this attempt
303
                this.emitGraphError(graph, lastError, context, {
4✔
304
                    ... details,
4✔
305
                    attempt: attempt + 1,
4✔
306
                    maxAttempts: this.errorRetryCount,
4✔
307
                });
4✔
308

309
                // Don't delay after the last attempt
310
                if (attempt < this.errorRetryCount - 1) {
4✔
311
                    // Exponential backoff
312
                    const delay = this.errorRetryDelay * Math.pow(2, attempt);
3✔
313
                    await new Promise((resolve) => setTimeout(resolve, delay));
3✔
314
                }
3✔
315
            }
4✔
316
        }
5✔
317

318
        // All attempts failed
319
        throw lastError ?? new Error("Operation failed with no recorded error");
2!
320
    }
2✔
321
}
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