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

graphty-org / graphty-element / 19792929756

30 Nov 2025 02:57AM UTC coverage: 86.308% (+3.9%) from 82.377%
19792929756

push

github

apowers313
docs: fix stories for chromatic

3676 of 4303 branches covered (85.43%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 2 files covered. (100.0%)

1093 existing lines in 30 files now uncovered.

17371 of 20083 relevant lines covered (86.5%)

7075.46 hits per line

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

90.24
/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
} from "../events";
20
import type {Graph} from "../Graph";
21
import type {GraphContext} from "./GraphContext";
22
import type {Manager} from "./interfaces";
23

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

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

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

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

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

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

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

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

75
    // Graph Events
76

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

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

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

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

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

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

151
    // Data Loading Events
152

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

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

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

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

237
    // Node Events
238

239
    emitNodeEvent(type: NodeEvent["type"], eventData: Omit<NodeEvent, "type">): void {
3✔
240
        const event = {type, ... eventData} as NodeEvent;
4,658✔
241
        this.nodeObservable.notifyObservers(event);
4,658✔
242
    }
4,658✔
243

244
    // Edge Events
245

246
    emitEdgeEvent(type: EdgeEvent["type"], eventData: Omit<EdgeEvent, "type">): void {
3✔
247
        const event = {type, ... eventData} as EdgeEvent;
8,173✔
248
        this.edgeObservable.notifyObservers(event);
8,173✔
249
    }
8,173✔
250

251
    // Event Listeners
252

253
    /**
254
     * Add a listener for a specific event type
255
     * Returns a symbol that can be used to remove the listener
256
     */
257
    addListener(type: EventType, callback: EventCallbackType): symbol {
3✔
258
        const id = Symbol("event-listener");
1,595✔
259

260
        switch (type) {
1,595✔
261
            case "graph-settled":
1,595✔
262
            case "error":
1,595✔
263
            case "data-loaded":
1,595✔
264
            case "data-added":
1,595✔
265
            case "layout-initialized":
1,595✔
266
            case "skybox-loaded":
1,595✔
267
            case "operation-queue-active":
1,595✔
268
            case "operation-queue-idle":
1,595✔
269
            case "operation-batch-complete":
1,595✔
270
            case "operation-start":
1,595✔
271
            case "operation-complete":
1,595✔
272
            case "operation-progress":
1,595✔
273
            case "operation-obsoleted":
1,595✔
274
            case "animation-progress":
1,595✔
275
            case "animation-cancelled":
1,595✔
276
            case "screenshot-enhancing":
1,595✔
277
            case "screenshot-ready":
1,595✔
278
            case "camera-state-changed":
1,595✔
279
            case "data-loading-progress":
1,595✔
280
            case "data-loading-error":
1,595✔
281
            case "data-loading-error-summary":
1,595✔
282
            case "data-loading-complete": {
1,595✔
283
                const observer = this.graphObservable.add((event) => {
1,587✔
284
                    if (event.type === type) {
412,087✔
285
                        callback(event);
14,654✔
286
                    }
14,654✔
287
                });
1,587✔
288
                this.observers.set(id, {
1,587✔
289
                    type: "graph",
1,587✔
290
                    observable: this.graphObservable,
1,587✔
291
                    observer,
1,587✔
292
                });
1,587✔
293
                break;
1,587✔
294
            }
1,587✔
295

296
            case "node-update-after":
1,595✔
297
            case "node-update-before":
1,595✔
298
            case "node-add-before": {
1,595✔
299
                const observer = this.nodeObservable.add((event) => {
5✔
300
                    if (event.type === type) {
4✔
301
                        callback(event);
3✔
302
                    }
3✔
303
                });
5✔
304
                this.observers.set(id, {
5✔
305
                    type: "node",
5✔
306
                    observable: this.nodeObservable,
5✔
307
                    observer,
5✔
308
                });
5✔
309
                break;
5✔
310
            }
5✔
311

312
            case "edge-update-after":
1,595✔
313
            case "edge-update-before":
1,595✔
314
            case "edge-add-before": {
1,595✔
315
                const observer = this.edgeObservable.add((event) => {
3✔
316
                    if (event.type === type) {
2✔
317
                        callback(event);
2✔
318
                    }
2✔
319
                });
3✔
320
                this.observers.set(id, {
3✔
321
                    type: "edge",
3✔
322
                    observable: this.edgeObservable,
3✔
323
                    observer,
3✔
324
                });
3✔
325
                break;
3✔
326
            }
3✔
327

328
            default:
1,595!
UNCOV
329
                throw new TypeError(`Unknown event type: ${type}`);
×
330
        }
1,595✔
331

332
        return id;
1,595✔
333
    }
1,595✔
334

335
    /**
336
     * Remove a listener by its ID
337
     */
338
    removeListener(id: symbol): boolean {
3✔
339
        const entry = this.observers.get(id);
34✔
340
        if (!entry) {
34✔
341
            return false;
1✔
342
        }
1✔
343

344
        entry.observable.remove(entry.observer);
33✔
345
        this.observers.delete(id);
33✔
346
        return true;
33✔
347
    }
34✔
348

349
    /**
350
     * Get the total number of registered listeners
351
     */
352
    listenerCount(): number {
3✔
353
        return this.observers.size;
2✔
354
    }
2✔
355

356
    /**
357
     * Add a one-time listener that automatically removes itself after firing
358
     */
359
    once(type: EventType, callback: EventCallbackType): symbol {
3✔
360
        const id = this.addListener(type, (event) => {
3✔
361
            callback(event);
2✔
362
            this.removeListener(id);
2✔
363
        });
3✔
364
        return id;
3✔
365
    }
3✔
366

367
    /**
368
     * Wait for a specific event to occur
369
     * Returns a promise that resolves with the event
370
     */
371
    waitFor(type: EventType, timeout?: number): Promise<GraphEvent | NodeEvent | EdgeEvent> {
3✔
372
        return new Promise((resolve, reject) => {
2✔
373
            const timeoutId = timeout ? setTimeout(() => {
2✔
374
                this.removeListener(listenerId);
1✔
375
                reject(new Error(`Timeout waiting for event: ${type}`));
1✔
376
            }, timeout) : undefined;
2!
377

378
            const listenerId = this.once(type, (event) => {
2✔
379
                if (timeoutId) {
1✔
380
                    clearTimeout(timeoutId);
1✔
381
                }
1✔
382

383
                resolve(event);
1✔
384
            });
2✔
385
        });
2✔
386
    }
2✔
387

388
    // Error Handling with Retry
389

390
    /**
391
     * Execute an async operation with automatic retry on failure
392
     * Emits error events for each failure
393
     */
394
    async withRetry<T>(
3✔
395
        operation: () => Promise<T>,
2✔
396
        context: GraphErrorEvent["context"],
2✔
397
        graph: Graph | GraphContext | null,
2✔
398
        details?: Record<string, unknown>,
2✔
399
    ): Promise<T> {
2✔
400
        let lastError: Error | undefined;
2✔
401

402
        for (let attempt = 0; attempt < this.errorRetryCount; attempt++) {
2✔
403
            try {
5✔
404
                return await operation();
5✔
405
            } catch (error) {
5✔
406
                lastError = error instanceof Error ? error : new Error(String(error));
4!
407

408
                // Emit error event for this attempt
409
                this.emitGraphError(graph, lastError, context, {
4✔
410
                    ... details,
4✔
411
                    attempt: attempt + 1,
4✔
412
                    maxAttempts: this.errorRetryCount,
4✔
413
                });
4✔
414

415
                // Don't delay after the last attempt
416
                if (attempt < this.errorRetryCount - 1) {
4✔
417
                    // Exponential backoff
418
                    const delay = this.errorRetryDelay * Math.pow(2, attempt);
3✔
419
                    await new Promise((resolve) => setTimeout(resolve, delay));
3✔
420
                }
3✔
421
            }
4✔
422
        }
5✔
423

424
        // All attempts failed
425
        throw lastError ?? new Error("Operation failed with no recorded error");
2!
426
    }
2✔
427
}
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