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

graphty-org / graphty-element / 18770573775

24 Oct 2025 05:27AM UTC coverage: 82.365% (+0.7%) from 81.618%
18770573775

push

github

apowers313
ci: fix ci, take 2

1221 of 1491 branches covered (81.89%)

Branch coverage included in aggregate %.

6588 of 7990 relevant lines covered (82.45%)

601.6 hits per line

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

97.34
/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>();
206✔
27
    private nodeObservable = new Observable<NodeEvent>();
206✔
28
    private edgeObservable = new Observable<EdgeEvent>();
206✔
29

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

35
    get onGraphError(): Observable<GraphErrorEvent> {
206✔
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, {
206✔
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
    }>();
206✔
48

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

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

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

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

71
    // Graph Events
72

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

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

170
        switch (type) {
507✔
171
            case "graph-settled":
507✔
172
            case "error":
507✔
173
            case "data-loaded":
507✔
174
            case "data-added":
507✔
175
            case "layout-initialized":
507✔
176
            case "skybox-loaded": {
507✔
177
                const observer = this.graphObservable.add((event) => {
499✔
178
                    if (event.type === type) {
9,880✔
179
                        callback(event);
552✔
180
                    }
552✔
181
                });
499✔
182
                this.observers.set(id, {
499✔
183
                    type: "graph",
499✔
184
                    observable: this.graphObservable,
499✔
185
                    observer,
499✔
186
                });
499✔
187
                break;
499✔
188
            }
499✔
189

190
            case "node-update-after":
507✔
191
            case "node-update-before":
507✔
192
            case "node-add-before": {
507✔
193
                const observer = this.nodeObservable.add((event) => {
5✔
194
                    if (event.type === type) {
4✔
195
                        callback(event);
3✔
196
                    }
3✔
197
                });
5✔
198
                this.observers.set(id, {
5✔
199
                    type: "node",
5✔
200
                    observable: this.nodeObservable,
5✔
201
                    observer,
5✔
202
                });
5✔
203
                break;
5✔
204
            }
5✔
205

206
            case "edge-update-after":
507✔
207
            case "edge-update-before":
507✔
208
            case "edge-add-before": {
507✔
209
                const observer = this.edgeObservable.add((event) => {
3✔
210
                    if (event.type === type) {
2✔
211
                        callback(event);
2✔
212
                    }
2✔
213
                });
3✔
214
                this.observers.set(id, {
3✔
215
                    type: "edge",
3✔
216
                    observable: this.edgeObservable,
3✔
217
                    observer,
3✔
218
                });
3✔
219
                break;
3✔
220
            }
3✔
221

222
            default:
507!
223
                throw new TypeError(`Unknown event type: ${type}`);
×
224
        }
507✔
225

226
        return id;
507✔
227
    }
507✔
228

229
    /**
230
     * Remove a listener by its ID
231
     */
232
    removeListener(id: symbol): boolean {
3✔
233
        const entry = this.observers.get(id);
21✔
234
        if (!entry) {
21✔
235
            return false;
1✔
236
        }
1✔
237

238
        entry.observable.remove(entry.observer);
20✔
239
        this.observers.delete(id);
20✔
240
        return true;
20✔
241
    }
21✔
242

243
    /**
244
     * Add a one-time listener that automatically removes itself after firing
245
     */
246
    once(type: EventType, callback: EventCallbackType): symbol {
3✔
247
        const id = this.addListener(type, (event) => {
3✔
248
            callback(event);
2✔
249
            this.removeListener(id);
2✔
250
        });
3✔
251
        return id;
3✔
252
    }
3✔
253

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

265
            const listenerId = this.once(type, (event) => {
2✔
266
                if (timeoutId) {
1✔
267
                    clearTimeout(timeoutId);
1✔
268
                }
1✔
269

270
                resolve(event);
1✔
271
            });
2✔
272
        });
2✔
273
    }
2✔
274

275
    // Error Handling with Retry
276

277
    /**
278
     * Execute an async operation with automatic retry on failure
279
     * Emits error events for each failure
280
     */
281
    async withRetry<T>(
3✔
282
        operation: () => Promise<T>,
2✔
283
        context: GraphErrorEvent["context"],
2✔
284
        graph: Graph | GraphContext | null,
2✔
285
        details?: Record<string, unknown>,
2✔
286
    ): Promise<T> {
2✔
287
        let lastError: Error | undefined;
2✔
288

289
        for (let attempt = 0; attempt < this.errorRetryCount; attempt++) {
2✔
290
            try {
5✔
291
                return await operation();
5✔
292
            } catch (error) {
5✔
293
                lastError = error instanceof Error ? error : new Error(String(error));
4!
294

295
                // Emit error event for this attempt
296
                this.emitGraphError(graph, lastError, context, {
4✔
297
                    ... details,
4✔
298
                    attempt: attempt + 1,
4✔
299
                    maxAttempts: this.errorRetryCount,
4✔
300
                });
4✔
301

302
                // Don't delay after the last attempt
303
                if (attempt < this.errorRetryCount - 1) {
4✔
304
                    // Exponential backoff
305
                    const delay = this.errorRetryDelay * Math.pow(2, attempt);
3✔
306
                    await new Promise((resolve) => setTimeout(resolve, delay));
3✔
307
                }
3✔
308
            }
4✔
309
        }
5✔
310

311
        // All attempts failed
312
        throw lastError ?? new Error("Operation failed with no recorded error");
2!
313
    }
2✔
314
}
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