@hawsen-the-first/interactiv 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +326 -0
  2. package/dist/animations.css +160 -0
  3. package/dist/index.d.ts +20 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +26 -0
  6. package/dist/src/animationBus.d.ts +30 -0
  7. package/dist/src/animationBus.d.ts.map +1 -0
  8. package/dist/src/animationBus.js +125 -0
  9. package/dist/src/appBuilder.d.ts +173 -0
  10. package/dist/src/appBuilder.d.ts.map +1 -0
  11. package/dist/src/appBuilder.js +957 -0
  12. package/dist/src/eventBus.d.ts +100 -0
  13. package/dist/src/eventBus.d.ts.map +1 -0
  14. package/dist/src/eventBus.js +326 -0
  15. package/dist/src/eventManager.d.ts +87 -0
  16. package/dist/src/eventManager.d.ts.map +1 -0
  17. package/dist/src/eventManager.js +455 -0
  18. package/dist/src/garbageCollector.d.ts +68 -0
  19. package/dist/src/garbageCollector.d.ts.map +1 -0
  20. package/dist/src/garbageCollector.js +169 -0
  21. package/dist/src/logger.d.ts +11 -0
  22. package/dist/src/logger.d.ts.map +1 -0
  23. package/dist/src/logger.js +15 -0
  24. package/dist/src/navigationManager.d.ts +105 -0
  25. package/dist/src/navigationManager.d.ts.map +1 -0
  26. package/dist/src/navigationManager.js +533 -0
  27. package/dist/src/screensaverManager.d.ts +66 -0
  28. package/dist/src/screensaverManager.d.ts.map +1 -0
  29. package/dist/src/screensaverManager.js +417 -0
  30. package/dist/src/settingsManager.d.ts +48 -0
  31. package/dist/src/settingsManager.d.ts.map +1 -0
  32. package/dist/src/settingsManager.js +317 -0
  33. package/dist/src/stateManager.d.ts +58 -0
  34. package/dist/src/stateManager.d.ts.map +1 -0
  35. package/dist/src/stateManager.js +278 -0
  36. package/dist/src/types.d.ts +32 -0
  37. package/dist/src/types.d.ts.map +1 -0
  38. package/dist/src/types.js +1 -0
  39. package/dist/utils/generateGuid.d.ts +2 -0
  40. package/dist/utils/generateGuid.d.ts.map +1 -0
  41. package/dist/utils/generateGuid.js +19 -0
  42. package/dist/utils/logger.d.ts +9 -0
  43. package/dist/utils/logger.d.ts.map +1 -0
  44. package/dist/utils/logger.js +42 -0
  45. package/dist/utils/template-helpers.d.ts +32 -0
  46. package/dist/utils/template-helpers.d.ts.map +1 -0
  47. package/dist/utils/template-helpers.js +24 -0
  48. package/package.json +59 -0
@@ -0,0 +1,455 @@
1
+ import { logger } from "./logger";
2
+ const log = logger;
3
+ export class EventManager {
4
+ shadowRoot;
5
+ abortController;
6
+ eventListeners = [];
7
+ selectorListeners = new Map(); // Track listeners by selector+method
8
+ dragStates = new Map();
9
+ swipeStates = new Map();
10
+ longPressTimers = new Map();
11
+ componentId;
12
+ constructor(shadowRoot, componentId) {
13
+ this.shadowRoot = shadowRoot;
14
+ this.componentId = componentId;
15
+ this.abortController = new AbortController();
16
+ }
17
+ /**
18
+ * Unified point interaction - handles both click and tap
19
+ */
20
+ point(selector, callback) {
21
+ log.trace(`EventManager.point() called for selector: ${selector} in component: ${this.componentId}`);
22
+ // Check if we already have listeners for this selector+method combination
23
+ const key = `point:${selector}`;
24
+ const existing = this.selectorListeners.get(key);
25
+ if (existing) {
26
+ const currentElements = Array.from(this.shadowRoot.querySelectorAll(selector));
27
+ const elementsChanged = existing.elements.length !== currentElements.length ||
28
+ existing.elements.some((el, i) => el !== currentElements[i]);
29
+ if (!elementsChanged) {
30
+ log.trace(`Point listeners already exist for selector: ${selector}, elements unchanged, skipping duplicate`);
31
+ return;
32
+ }
33
+ else {
34
+ log.trace(`DOM elements changed for selector: ${selector}, clearing old listeners and re-attaching`);
35
+ // Elements changed, clear old listeners
36
+ this.clearSelectorListeners(selector, "point");
37
+ }
38
+ }
39
+ const elements = this.shadowRoot.querySelectorAll(selector);
40
+ log.trace(`Found ${elements.length} elements for selector: ${selector}`);
41
+ // Store the selector listener mapping
42
+ this.selectorListeners.set(key, {
43
+ selector,
44
+ method: "point",
45
+ callback,
46
+ elements: Array.from(elements),
47
+ });
48
+ elements.forEach((element) => {
49
+ // Handle mouse click
50
+ this.addEventListenerWithTracking(element, "click", (e) => {
51
+ const mouseEvent = e;
52
+ mouseEvent.preventDefault();
53
+ const data = this.createPointerEventData(mouseEvent, "mouse");
54
+ callback(data);
55
+ }, undefined, selector, "point");
56
+ // Handle touch tap (short touch)
57
+ let touchStartTime = 0;
58
+ let pixelStartX, pixelStartY;
59
+ this.addEventListenerWithTracking(element, "touchstart", (e) => {
60
+ log.trace("Touch event ", e);
61
+ const touchEvent = e;
62
+ pixelStartX = touchEvent.changedTouches[0]?.clientX;
63
+ pixelStartY = touchEvent.changedTouches[0]?.clientY;
64
+ touchStartTime = Date.now();
65
+ }, { passive: true }, selector, "point");
66
+ this.addEventListenerWithTracking(element, "touchend", (e) => {
67
+ const touchEvent = e;
68
+ const touchDuration = Date.now() - touchStartTime;
69
+ log.trace("Touch event ", e);
70
+ if (touchDuration < 300 &&
71
+ !this.movementThresholdMet(pixelStartX, pixelStartY, touchEvent.changedTouches[0].clientX, touchEvent.changedTouches[0].clientY)) {
72
+ // Short tap
73
+ touchEvent.preventDefault();
74
+ const data = this.createPointerEventData(touchEvent, "touch");
75
+ callback(data);
76
+ }
77
+ }, undefined, selector, "point");
78
+ });
79
+ log.trace(`Point listeners added for selector: ${selector}`, {
80
+ componentId: this.componentId,
81
+ });
82
+ }
83
+ /**
84
+ * Un-point interaction - handles touch and click events on the background to de-select an active element
85
+ */
86
+ unpoint(selector, callback) {
87
+ const body = document.querySelector("body");
88
+ const key = `unpoint:${selector}`;
89
+ // Store the selector listener mapping
90
+ this.selectorListeners.set(key, {
91
+ selector,
92
+ method: "unpoint",
93
+ callback,
94
+ elements: [body],
95
+ });
96
+ this.addEventListenerWithTracking(body, "click", (e) => {
97
+ const mouseEvent = e;
98
+ mouseEvent.preventDefault();
99
+ const data = this.createPointerEventData(mouseEvent, "mouse");
100
+ callback(data);
101
+ }, { once: true }, selector, "point");
102
+ // Handle touch tap (short touch)
103
+ let touchStartTime = 0;
104
+ this.addEventListenerWithTracking(body, "touchstart", (_) => {
105
+ touchStartTime = Date.now();
106
+ }, { passive: true, once: true }, selector, "point");
107
+ this.addEventListenerWithTracking(body, "touchend", (e) => {
108
+ const touchEvent = e;
109
+ const touchDuration = Date.now() - touchStartTime;
110
+ if (touchDuration < 300) {
111
+ // Short tap
112
+ touchEvent.preventDefault();
113
+ const data = this.createPointerEventData(touchEvent, "touch");
114
+ callback(data);
115
+ }
116
+ }, { once: true }, selector, "point");
117
+ log.trace(`Un-Point listeners added for selector: ${selector}`, {
118
+ componentId: this.componentId,
119
+ });
120
+ }
121
+ /**
122
+ * Drag interaction - handles both mouse drag and touch drag
123
+ */
124
+ drag(selector, callbacks) {
125
+ const elements = this.shadowRoot.querySelectorAll(selector);
126
+ elements.forEach((element) => {
127
+ // Mouse drag
128
+ this.addEventListener(element, "mousedown", (e) => {
129
+ this.startDrag(element, e, "mouse", callbacks);
130
+ });
131
+ // Touch drag
132
+ this.addEventListener(element, "touchstart", (e) => {
133
+ this.startDrag(element, e, "touch", callbacks);
134
+ }, { passive: false });
135
+ });
136
+ // Global mouse move and up listeners
137
+ this.addEventListener(document, "mousemove", (e) => {
138
+ this.handleDragMove(e, "mouse");
139
+ });
140
+ this.addEventListener(document, "mouseup", (e) => {
141
+ this.handleDragEnd(e, "mouse");
142
+ });
143
+ // Global touch move and end listeners
144
+ this.addEventListener(document, "touchmove", (e) => {
145
+ this.handleDragMove(e, "touch");
146
+ }, { passive: false });
147
+ this.addEventListener(document, "touchend", (e) => {
148
+ this.handleDragEnd(e, "touch");
149
+ });
150
+ log.trace(`Drag listeners added for selector: ${selector}`, {
151
+ componentId: this.componentId,
152
+ });
153
+ }
154
+ /**
155
+ * Hover interaction - mouse enter/leave with touch fallback
156
+ */
157
+ hover(selector, callbacks) {
158
+ // Check if we already have listeners for this selector+method combination
159
+ const key = `hover:${selector}`;
160
+ if (this.selectorListeners.has(key)) {
161
+ log.trace(`Hover listeners already exist for selector: ${selector}, skipping duplicate`, {
162
+ componentId: this.componentId,
163
+ });
164
+ return;
165
+ }
166
+ const elements = this.shadowRoot.querySelectorAll(selector);
167
+ // Store the selector listener mapping
168
+ this.selectorListeners.set(key, {
169
+ selector,
170
+ method: "hover",
171
+ callback: callbacks,
172
+ elements: Array.from(elements),
173
+ });
174
+ elements.forEach((element) => {
175
+ this.addEventListenerWithTracking(element, "mouseenter", (e) => {
176
+ if (callbacks.enter) {
177
+ const data = this.createPointerEventData(e, "mouse");
178
+ callbacks.enter(data);
179
+ }
180
+ }, undefined, selector, "hover");
181
+ this.addEventListenerWithTracking(element, "mouseleave", (e) => {
182
+ if (callbacks.leave) {
183
+ const data = this.createPointerEventData(e, "mouse");
184
+ callbacks.leave(data);
185
+ }
186
+ }, undefined, selector, "hover");
187
+ // Touch fallback - simulate hover with touch
188
+ this.addEventListenerWithTracking(element, "touchstart", (e) => {
189
+ if (callbacks.enter) {
190
+ const data = this.createPointerEventData(e, "touch");
191
+ callbacks.enter(data);
192
+ }
193
+ }, { passive: true }, selector, "hover");
194
+ });
195
+ log.trace(`Hover listeners added for selector: ${selector}`, {
196
+ componentId: this.componentId,
197
+ });
198
+ }
199
+ /**
200
+ * Long press interaction - works for both mouse and touch
201
+ */
202
+ longPress(selector, callback, duration = 500) {
203
+ const elements = this.shadowRoot.querySelectorAll(selector);
204
+ elements.forEach((element) => {
205
+ // Mouse long press
206
+ this.addEventListener(element, "mousedown", (e) => {
207
+ this.startLongPress(element, e, "mouse", callback, duration);
208
+ });
209
+ this.addEventListener(element, "mouseup", () => {
210
+ this.cancelLongPress(element);
211
+ });
212
+ this.addEventListener(element, "mouseleave", () => {
213
+ this.cancelLongPress(element);
214
+ });
215
+ // Touch long press
216
+ this.addEventListener(element, "touchstart", (e) => {
217
+ this.startLongPress(element, e, "touch", callback, duration);
218
+ }, { passive: true });
219
+ this.addEventListener(element, "touchend", () => {
220
+ this.cancelLongPress(element);
221
+ });
222
+ this.addEventListener(element, "touchcancel", () => {
223
+ this.cancelLongPress(element);
224
+ });
225
+ });
226
+ log.trace(`Long press listeners added for selector: ${selector}`, {
227
+ componentId: this.componentId,
228
+ });
229
+ }
230
+ /**
231
+ * Swipe gesture detection
232
+ */
233
+ swipe(selector, callbacks, threshold = 50) {
234
+ const elements = this.shadowRoot.querySelectorAll(selector);
235
+ elements.forEach((element) => {
236
+ // Touch swipe
237
+ this.addEventListener(element, "touchstart", (e) => {
238
+ this.startSwipe(element, e, callbacks);
239
+ }, { passive: true });
240
+ this.addEventListener(element, "touchend", (e) => {
241
+ this.endSwipe(element, e, callbacks, threshold);
242
+ });
243
+ // Mouse swipe (drag-based)
244
+ this.addEventListener(element, "mousedown", (e) => {
245
+ this.startSwipe(element, e, callbacks);
246
+ });
247
+ this.addEventListener(document, "mouseup", (e) => {
248
+ this.endSwipe(element, e, callbacks, threshold);
249
+ });
250
+ });
251
+ log.trace(`Swipe listeners added for selector: ${selector}`, {
252
+ componentId: this.componentId,
253
+ });
254
+ }
255
+ /**
256
+ * Add a custom event listener with automatic cleanup
257
+ */
258
+ addEventListener(element, type, listener, options) {
259
+ const finalOptions = {
260
+ ...options,
261
+ signal: this.abortController.signal,
262
+ };
263
+ element.addEventListener(type, listener, finalOptions);
264
+ this.eventListeners.push({
265
+ element: element,
266
+ type,
267
+ listener,
268
+ options: finalOptions,
269
+ });
270
+ }
271
+ /**
272
+ * Add an event listener with tracking for selector and method
273
+ */
274
+ addEventListenerWithTracking(element, type, listener, options, selector, method) {
275
+ const finalOptions = {
276
+ ...options,
277
+ signal: this.abortController.signal,
278
+ };
279
+ element.addEventListener(type, listener, finalOptions);
280
+ this.eventListeners.push({
281
+ element: element,
282
+ type,
283
+ listener,
284
+ options: finalOptions,
285
+ selector,
286
+ method,
287
+ });
288
+ }
289
+ /**
290
+ * Remove a specific event listener
291
+ */
292
+ removeEventListener(element, type, listener) {
293
+ element.removeEventListener(type, listener);
294
+ const index = this.eventListeners.findIndex((record) => record.element === element && record.type === type && record.listener === listener);
295
+ if (index !== -1) {
296
+ this.eventListeners.splice(index, 1);
297
+ }
298
+ }
299
+ /**
300
+ * Clean up listeners for a specific selector and method
301
+ */
302
+ clearSelectorListeners(selector, method) {
303
+ const key = `${method}:${selector}`;
304
+ if (this.selectorListeners.has(key)) {
305
+ // Remove from tracking
306
+ this.selectorListeners.delete(key);
307
+ // Remove actual event listeners for this selector+method
308
+ this.eventListeners = this.eventListeners.filter((record) => {
309
+ if (record.selector === selector && record.method === method) {
310
+ // Remove the actual event listener
311
+ record.element.removeEventListener(record.type, record.listener);
312
+ return false; // Remove from array
313
+ }
314
+ return true; // Keep in array
315
+ });
316
+ log.trace(`Cleared listeners for selector: ${selector}, method: ${method}`, { componentId: this.componentId });
317
+ }
318
+ }
319
+ /**
320
+ * Clean up all event listeners
321
+ */
322
+ destroy() {
323
+ this.abortController.abort();
324
+ this.eventListeners.length = 0;
325
+ this.selectorListeners.clear();
326
+ this.dragStates.clear();
327
+ this.swipeStates.clear();
328
+ // Clear any pending long press timers
329
+ this.longPressTimers.forEach((timerId) => clearTimeout(timerId));
330
+ this.longPressTimers.clear();
331
+ log.trace(`EventManager destroyed for component: ${this.componentId}`);
332
+ }
333
+ // Private helper methods
334
+ createPointerEventData(event, type) {
335
+ let x, y;
336
+ if (type === "touch" && "touches" in event) {
337
+ const touch = event.touches[0] || event.changedTouches[0];
338
+ x = touch.clientX;
339
+ y = touch.clientY;
340
+ }
341
+ else if ("clientX" in event) {
342
+ x = event.clientX;
343
+ y = event.clientY;
344
+ }
345
+ else {
346
+ x = 0;
347
+ y = 0;
348
+ }
349
+ return {
350
+ x,
351
+ y,
352
+ target: event.target,
353
+ originalEvent: event,
354
+ type,
355
+ };
356
+ }
357
+ startDrag(element, event, type, callbacks) {
358
+ const data = this.createPointerEventData(event, type);
359
+ const dragState = {
360
+ isDragging: true,
361
+ startX: data.x,
362
+ startY: data.y,
363
+ element,
364
+ callbacks,
365
+ };
366
+ this.dragStates.set(element, dragState);
367
+ if (callbacks.start) {
368
+ callbacks.start(data);
369
+ }
370
+ event.preventDefault();
371
+ }
372
+ handleDragMove(event, type) {
373
+ this.dragStates.forEach((dragState, _) => {
374
+ if (dragState.isDragging && dragState.callbacks.move) {
375
+ const data = this.createPointerEventData(event, type);
376
+ dragState.callbacks.move(data);
377
+ }
378
+ });
379
+ }
380
+ handleDragEnd(event, type) {
381
+ this.dragStates.forEach((dragState, element) => {
382
+ if (dragState.isDragging) {
383
+ dragState.isDragging = false;
384
+ if (dragState.callbacks.end) {
385
+ const data = this.createPointerEventData(event, type);
386
+ dragState.callbacks.end(data);
387
+ }
388
+ this.dragStates.delete(element);
389
+ }
390
+ });
391
+ }
392
+ movementThresholdMet(startX, startY, endX, endY) {
393
+ const movementThreshold = 5; // Default movement threshold in pixels
394
+ const distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
395
+ return distance > movementThreshold;
396
+ }
397
+ startLongPress(element, event, type, callback, duration) {
398
+ this.cancelLongPress(element); // Cancel any existing timer
399
+ const timerId = window.setTimeout(() => {
400
+ const data = this.createPointerEventData(event, type);
401
+ callback(data);
402
+ this.longPressTimers.delete(element);
403
+ }, duration);
404
+ this.longPressTimers.set(element, timerId);
405
+ }
406
+ cancelLongPress(element) {
407
+ const timerId = this.longPressTimers.get(element);
408
+ if (timerId) {
409
+ clearTimeout(timerId);
410
+ this.longPressTimers.delete(element);
411
+ }
412
+ }
413
+ startSwipe(element, event, callbacks) {
414
+ const data = this.createPointerEventData(event, "touch");
415
+ const swipeState = {
416
+ startX: data.x,
417
+ startY: data.y,
418
+ startTime: Date.now(),
419
+ element,
420
+ callbacks,
421
+ };
422
+ this.swipeStates.set(element, swipeState);
423
+ }
424
+ endSwipe(element, event, callbacks, threshold) {
425
+ const swipeState = this.swipeStates.get(element);
426
+ if (!swipeState)
427
+ return;
428
+ const data = this.createPointerEventData(event, "touch");
429
+ const deltaX = data.x - swipeState.startX;
430
+ const deltaY = data.y - swipeState.startY;
431
+ const deltaTime = Date.now() - swipeState.startTime;
432
+ // Only consider it a swipe if it was fast enough (< 300ms) and moved enough
433
+ if (deltaTime < 300 && (Math.abs(deltaX) > threshold || Math.abs(deltaY) > threshold)) {
434
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
435
+ // Horizontal swipe
436
+ if (deltaX > 0 && callbacks.right) {
437
+ callbacks.right(data);
438
+ }
439
+ else if (deltaX < 0 && callbacks.left) {
440
+ callbacks.left(data);
441
+ }
442
+ }
443
+ else {
444
+ // Vertical swipe
445
+ if (deltaY > 0 && callbacks.down) {
446
+ callbacks.down(data);
447
+ }
448
+ else if (deltaY < 0 && callbacks.up) {
449
+ callbacks.up(data);
450
+ }
451
+ }
452
+ }
453
+ this.swipeStates.delete(element);
454
+ }
455
+ }
@@ -0,0 +1,68 @@
1
+ import type { EventOrchestrator } from "./eventBus";
2
+ import type { AnimationManager } from "./animationBus";
3
+ export interface GarbageCollectionStats {
4
+ timestamp: number;
5
+ animationsCleaned: number;
6
+ expiredEventsCleaned: number;
7
+ orphanedTransitionsCleaned: number;
8
+ totalListeners: number;
9
+ queueSize: number;
10
+ activeAnimations: number;
11
+ activeTransitions: number;
12
+ }
13
+ export declare class GarbageCollector {
14
+ private orchestrator;
15
+ private animationManager?;
16
+ private cleanupInterval;
17
+ private stats;
18
+ private readonly MAX_STATS_HISTORY;
19
+ constructor(orchestrator: EventOrchestrator, animationManager?: AnimationManager);
20
+ /**
21
+ * Start automatic garbage collection at specified interval
22
+ * @param intervalMinutes - How often to run cleanup (default: 5 minutes)
23
+ */
24
+ startAutoCleanup(intervalMinutes?: number): void;
25
+ /**
26
+ * Stop automatic garbage collection
27
+ */
28
+ stopAutoCleanup(): void;
29
+ /**
30
+ * Manually trigger a garbage collection run
31
+ */
32
+ runCleanup(): GarbageCollectionStats;
33
+ /**
34
+ * Get statistics from previous cleanup runs
35
+ */
36
+ getStats(): GarbageCollectionStats[];
37
+ /**
38
+ * Get the most recent cleanup stats
39
+ */
40
+ getLatestStats(): GarbageCollectionStats | null;
41
+ /**
42
+ * Get a summary report of memory usage trends
43
+ */
44
+ getMemoryReport(): {
45
+ averageAnimations: number;
46
+ averageListeners: number;
47
+ averageQueueSize: number;
48
+ averageTransitions: number;
49
+ totalCleanups: number;
50
+ totalItemsCleaned: number;
51
+ };
52
+ /**
53
+ * Clear all stats history
54
+ */
55
+ clearStats(): void;
56
+ /**
57
+ * Destroy the garbage collector and stop auto cleanup
58
+ */
59
+ destroy(): void;
60
+ }
61
+ /**
62
+ * Create and configure a garbage collector instance
63
+ *
64
+ * Note: NavigationManager is automatically retrieved via singleton pattern
65
+ * when needed during cleanup operations.
66
+ */
67
+ export declare function createGarbageCollector(orchestrator: EventOrchestrator, animationManager?: AnimationManager, autoStart?: boolean, intervalMinutes?: number): GarbageCollector;
68
+ //# sourceMappingURL=garbageCollector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"garbageCollector.d.ts","sourceRoot":"","sources":["../../src/garbageCollector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKvD,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B,EAAE,MAAM,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAM;gBAGtC,YAAY,EAAE,iBAAiB,EAC/B,gBAAgB,CAAC,EAAE,gBAAgB;IAMrC;;;OAGG;IACI,gBAAgB,CAAC,eAAe,GAAE,MAAU,GAAG,IAAI;IAiB1D;;OAEG;IACI,eAAe,IAAI,IAAI;IAQ9B;;OAEG;IACI,UAAU,IAAI,sBAAsB;IAqE3C;;OAEG;IACI,QAAQ,IAAI,sBAAsB,EAAE;IAI3C;;OAEG;IACI,cAAc,IAAI,sBAAsB,GAAG,IAAI;IAItD;;OAEG;IACI,eAAe,IAAI;QACxB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,gBAAgB,EAAE,MAAM,CAAC;QACzB,gBAAgB,EAAE,MAAM,CAAC;QACzB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,iBAAiB,EAAE,MAAM,CAAC;KAC3B;IAiCD;;OAEG;IACI,UAAU,IAAI,IAAI;IAIzB;;OAEG;IACI,OAAO,IAAI,IAAI;CAKvB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,iBAAiB,EAC/B,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,SAAS,GAAE,OAAc,EACzB,eAAe,GAAE,MAAU,GAC1B,gBAAgB,CAQlB"}
@@ -0,0 +1,169 @@
1
+ import { logger } from "./logger";
2
+ import { NavigationManager } from "./navigationManager";
3
+ const log = logger;
4
+ export class GarbageCollector {
5
+ orchestrator;
6
+ animationManager;
7
+ cleanupInterval = null;
8
+ stats = [];
9
+ MAX_STATS_HISTORY = 50; // Keep last 50 cleanup runs
10
+ constructor(orchestrator, animationManager) {
11
+ this.orchestrator = orchestrator;
12
+ this.animationManager = animationManager;
13
+ }
14
+ /**
15
+ * Start automatic garbage collection at specified interval
16
+ * @param intervalMinutes - How often to run cleanup (default: 5 minutes)
17
+ */
18
+ startAutoCleanup(intervalMinutes = 5) {
19
+ if (this.cleanupInterval !== null) {
20
+ log.warn("Auto cleanup already running");
21
+ return;
22
+ }
23
+ const intervalMs = intervalMinutes * 60 * 1000;
24
+ log.trace(`Starting automatic garbage collection every ${intervalMinutes} minute(s)`);
25
+ this.cleanupInterval = window.setInterval(() => {
26
+ this.runCleanup();
27
+ }, intervalMs);
28
+ // Run initial cleanup
29
+ this.runCleanup();
30
+ }
31
+ /**
32
+ * Stop automatic garbage collection
33
+ */
34
+ stopAutoCleanup() {
35
+ if (this.cleanupInterval !== null) {
36
+ clearInterval(this.cleanupInterval);
37
+ this.cleanupInterval = null;
38
+ log.trace("Automatic garbage collection stopped");
39
+ }
40
+ }
41
+ /**
42
+ * Manually trigger a garbage collection run
43
+ */
44
+ runCleanup() {
45
+ const startTime = Date.now();
46
+ log.trace("Running garbage collection...");
47
+ const stats = {
48
+ timestamp: startTime,
49
+ animationsCleaned: 0,
50
+ expiredEventsCleaned: 0,
51
+ orphanedTransitionsCleaned: 0,
52
+ totalListeners: 0,
53
+ queueSize: 0,
54
+ activeAnimations: 0,
55
+ activeTransitions: 0,
56
+ };
57
+ // Clean up stale animations
58
+ if (this.animationManager) {
59
+ const beforeAnimations = this.animationManager.getActiveAnimationCount();
60
+ this.animationManager.cleanupStaleAnimations();
61
+ const afterAnimations = this.animationManager.getActiveAnimationCount();
62
+ stats.animationsCleaned = beforeAnimations - afterAnimations;
63
+ stats.activeAnimations = afterAnimations;
64
+ }
65
+ // Clean up expired events from queue
66
+ const expiredEvents = this.orchestrator.cleanupExpiredEvents();
67
+ stats.expiredEventsCleaned = expiredEvents;
68
+ stats.queueSize = this.orchestrator.getQueueSize();
69
+ stats.totalListeners = this.orchestrator.getTotalListenerCount();
70
+ // Clean up orphaned navigation transitions - get singleton instance
71
+ const navigationManager = NavigationManager.getInstance();
72
+ if (navigationManager) {
73
+ const beforeTransitions = navigationManager.getActiveTransitionCount();
74
+ navigationManager.cleanupOrphanedTransitions();
75
+ const afterTransitions = navigationManager.getActiveTransitionCount();
76
+ stats.orphanedTransitionsCleaned = beforeTransitions - afterTransitions;
77
+ stats.activeTransitions = afterTransitions;
78
+ }
79
+ const duration = Date.now() - startTime;
80
+ // Log summary
81
+ const totalCleaned = stats.animationsCleaned + stats.expiredEventsCleaned + stats.orphanedTransitionsCleaned;
82
+ if (totalCleaned > 0) {
83
+ log.trace(`Garbage collection complete in ${duration}ms. Cleaned: ${stats.animationsCleaned} animations, ` +
84
+ `${stats.expiredEventsCleaned} expired events, ${stats.orphanedTransitionsCleaned} transitions. ` +
85
+ `Active: ${stats.activeAnimations} animations, ${stats.activeTransitions} transitions, ` +
86
+ `${stats.totalListeners} listeners, queue size: ${stats.queueSize}`);
87
+ }
88
+ else {
89
+ log.trace(`Garbage collection complete in ${duration}ms. Nothing to clean. ` +
90
+ `Active: ${stats.activeAnimations} animations, ${stats.activeTransitions} transitions, ` +
91
+ `${stats.totalListeners} listeners, queue size: ${stats.queueSize}`);
92
+ }
93
+ // Store stats
94
+ this.stats.push(stats);
95
+ if (this.stats.length > this.MAX_STATS_HISTORY) {
96
+ this.stats.shift(); // Remove oldest
97
+ }
98
+ return stats;
99
+ }
100
+ /**
101
+ * Get statistics from previous cleanup runs
102
+ */
103
+ getStats() {
104
+ return [...this.stats];
105
+ }
106
+ /**
107
+ * Get the most recent cleanup stats
108
+ */
109
+ getLatestStats() {
110
+ return this.stats.length > 0 ? this.stats[this.stats.length - 1] : null;
111
+ }
112
+ /**
113
+ * Get a summary report of memory usage trends
114
+ */
115
+ getMemoryReport() {
116
+ if (this.stats.length === 0) {
117
+ return {
118
+ averageAnimations: 0,
119
+ averageListeners: 0,
120
+ averageQueueSize: 0,
121
+ averageTransitions: 0,
122
+ totalCleanups: 0,
123
+ totalItemsCleaned: 0,
124
+ };
125
+ }
126
+ const sum = this.stats.reduce((acc, stat) => ({
127
+ animations: acc.animations + stat.activeAnimations,
128
+ listeners: acc.listeners + stat.totalListeners,
129
+ queueSize: acc.queueSize + stat.queueSize,
130
+ transitions: acc.transitions + stat.activeTransitions,
131
+ cleaned: acc.cleaned + stat.animationsCleaned + stat.expiredEventsCleaned + stat.orphanedTransitionsCleaned,
132
+ }), { animations: 0, listeners: 0, queueSize: 0, transitions: 0, cleaned: 0 });
133
+ return {
134
+ averageAnimations: sum.animations / this.stats.length,
135
+ averageListeners: sum.listeners / this.stats.length,
136
+ averageQueueSize: sum.queueSize / this.stats.length,
137
+ averageTransitions: sum.transitions / this.stats.length,
138
+ totalCleanups: this.stats.length,
139
+ totalItemsCleaned: sum.cleaned,
140
+ };
141
+ }
142
+ /**
143
+ * Clear all stats history
144
+ */
145
+ clearStats() {
146
+ this.stats = [];
147
+ }
148
+ /**
149
+ * Destroy the garbage collector and stop auto cleanup
150
+ */
151
+ destroy() {
152
+ this.stopAutoCleanup();
153
+ this.clearStats();
154
+ log.trace("GarbageCollector destroyed");
155
+ }
156
+ }
157
+ /**
158
+ * Create and configure a garbage collector instance
159
+ *
160
+ * Note: NavigationManager is automatically retrieved via singleton pattern
161
+ * when needed during cleanup operations.
162
+ */
163
+ export function createGarbageCollector(orchestrator, animationManager, autoStart = true, intervalMinutes = 5) {
164
+ const gc = new GarbageCollector(orchestrator, animationManager);
165
+ if (autoStart) {
166
+ gc.startAutoCleanup(intervalMinutes);
167
+ }
168
+ return gc;
169
+ }