@sc4rfurryx/proteusjs 1.0.0
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.
- package/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- package/src/utils/version.ts +24 -0
@@ -0,0 +1,740 @@
|
|
1
|
+
/**
|
2
|
+
* Efficient Resizing & Event Handling for ProteusJS
|
3
|
+
* Debounced resize events with RAF timing, batch DOM operations, and 60fps performance
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface EventHandlerConfig {
|
7
|
+
debounceDelay: number;
|
8
|
+
throttleDelay: number;
|
9
|
+
useRAF: boolean;
|
10
|
+
batchOperations: boolean;
|
11
|
+
passiveListeners: boolean;
|
12
|
+
intersectionBased: boolean;
|
13
|
+
performanceTarget: number; // Target frame time in ms (16.67ms for 60fps)
|
14
|
+
maxBatchSize: number;
|
15
|
+
priorityLevels: ('high' | 'normal' | 'low')[];
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface EventOperation {
|
19
|
+
id: string;
|
20
|
+
element: Element;
|
21
|
+
callback: () => void;
|
22
|
+
priority: 'high' | 'normal' | 'low';
|
23
|
+
timestamp: number;
|
24
|
+
cost: number; // Estimated execution cost in ms
|
25
|
+
}
|
26
|
+
|
27
|
+
export interface PerformanceMetrics {
|
28
|
+
frameTime: number;
|
29
|
+
fps: number;
|
30
|
+
operationsPerFrame: number;
|
31
|
+
queueSize: number;
|
32
|
+
droppedFrames: number;
|
33
|
+
averageLatency: number;
|
34
|
+
}
|
35
|
+
|
36
|
+
export class EfficientEventHandler {
|
37
|
+
private config: Required<EventHandlerConfig>;
|
38
|
+
private resizeObserver: ResizeObserver | null = null;
|
39
|
+
private intersectionObserver: IntersectionObserver | null = null;
|
40
|
+
private operationQueue: Map<string, EventOperation> = new Map();
|
41
|
+
private batchQueue: EventOperation[] = [];
|
42
|
+
private rafId: number | null = null;
|
43
|
+
private lastFrameTime: number = 0;
|
44
|
+
private frameCount: number = 0;
|
45
|
+
private metrics: PerformanceMetrics;
|
46
|
+
private isProcessing: boolean = false;
|
47
|
+
private visibleElements: Set<Element> = new Set();
|
48
|
+
|
49
|
+
// Debounce and throttle timers
|
50
|
+
private debounceTimers: Map<string, number> = new Map();
|
51
|
+
private throttleTimers: Map<string, number> = new Map();
|
52
|
+
private lastThrottleTime: Map<string, number> = new Map();
|
53
|
+
|
54
|
+
constructor(config: Partial<EventHandlerConfig> = {}) {
|
55
|
+
this.config = {
|
56
|
+
debounceDelay: 16, // ~1 frame at 60fps
|
57
|
+
throttleDelay: 16,
|
58
|
+
useRAF: true,
|
59
|
+
batchOperations: true,
|
60
|
+
passiveListeners: true,
|
61
|
+
intersectionBased: true,
|
62
|
+
performanceTarget: 16.67, // 60fps target
|
63
|
+
maxBatchSize: 10,
|
64
|
+
priorityLevels: ['high', 'normal', 'low'],
|
65
|
+
...config
|
66
|
+
};
|
67
|
+
|
68
|
+
this.metrics = this.createInitialMetrics();
|
69
|
+
this.setupPerformanceMonitoring();
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Initialize the efficient event handling system
|
74
|
+
*/
|
75
|
+
public initialize(): void {
|
76
|
+
this.setupResizeObserver();
|
77
|
+
this.setupIntersectionObserver();
|
78
|
+
this.startProcessingLoop();
|
79
|
+
}
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Clean up and destroy the event handler
|
83
|
+
*/
|
84
|
+
public destroy(): void {
|
85
|
+
this.stopProcessingLoop();
|
86
|
+
this.cleanupObservers();
|
87
|
+
this.clearQueues();
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Register element for efficient resize handling
|
92
|
+
*/
|
93
|
+
public observeResize(
|
94
|
+
element: Element,
|
95
|
+
callback: (entry: ResizeObserverEntry) => void,
|
96
|
+
priority: 'high' | 'normal' | 'low' = 'normal'
|
97
|
+
): string {
|
98
|
+
const operationId = this.generateOperationId('resize', element);
|
99
|
+
|
100
|
+
// Wrap callback for batching
|
101
|
+
const wrappedCallback = () => {
|
102
|
+
if (this.resizeObserver) {
|
103
|
+
// Get the latest resize entry for this element
|
104
|
+
const rect = element.getBoundingClientRect();
|
105
|
+
const entry = {
|
106
|
+
target: element,
|
107
|
+
contentRect: rect,
|
108
|
+
borderBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
|
109
|
+
contentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
|
110
|
+
devicePixelContentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }]
|
111
|
+
} as ResizeObserverEntry;
|
112
|
+
|
113
|
+
callback(entry);
|
114
|
+
}
|
115
|
+
};
|
116
|
+
|
117
|
+
// Add to operation queue
|
118
|
+
this.addOperation({
|
119
|
+
id: operationId,
|
120
|
+
element,
|
121
|
+
callback: wrappedCallback,
|
122
|
+
priority,
|
123
|
+
timestamp: performance.now(),
|
124
|
+
cost: this.estimateOperationCost('resize')
|
125
|
+
});
|
126
|
+
|
127
|
+
// Start observing with ResizeObserver
|
128
|
+
if (this.resizeObserver) {
|
129
|
+
this.resizeObserver.observe(element);
|
130
|
+
}
|
131
|
+
|
132
|
+
return operationId;
|
133
|
+
}
|
134
|
+
|
135
|
+
/**
|
136
|
+
* Register element for intersection-based activation
|
137
|
+
*/
|
138
|
+
public observeIntersection(
|
139
|
+
element: Element,
|
140
|
+
callback: (entry: IntersectionObserverEntry) => void,
|
141
|
+
_options: IntersectionObserverInit = {}
|
142
|
+
): string {
|
143
|
+
const operationId = this.generateOperationId('intersection', element);
|
144
|
+
|
145
|
+
// Wrap callback for batching
|
146
|
+
const wrappedCallback = () => {
|
147
|
+
const rect = element.getBoundingClientRect();
|
148
|
+
const rootBounds = document.documentElement.getBoundingClientRect();
|
149
|
+
|
150
|
+
const entry = {
|
151
|
+
target: element,
|
152
|
+
boundingClientRect: rect,
|
153
|
+
rootBounds,
|
154
|
+
intersectionRect: rect,
|
155
|
+
intersectionRatio: this.calculateIntersectionRatio(rect, rootBounds),
|
156
|
+
isIntersecting: this.isElementIntersecting(rect, rootBounds),
|
157
|
+
time: performance.now()
|
158
|
+
} as IntersectionObserverEntry;
|
159
|
+
|
160
|
+
callback(entry);
|
161
|
+
};
|
162
|
+
|
163
|
+
this.addOperation({
|
164
|
+
id: operationId,
|
165
|
+
element,
|
166
|
+
callback: wrappedCallback,
|
167
|
+
priority: 'normal',
|
168
|
+
timestamp: performance.now(),
|
169
|
+
cost: this.estimateOperationCost('intersection')
|
170
|
+
});
|
171
|
+
|
172
|
+
// Start observing with IntersectionObserver
|
173
|
+
if (this.intersectionObserver) {
|
174
|
+
this.intersectionObserver.observe(element);
|
175
|
+
}
|
176
|
+
|
177
|
+
return operationId;
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Add debounced event listener
|
182
|
+
*/
|
183
|
+
public addDebouncedListener(
|
184
|
+
element: Element,
|
185
|
+
event: string,
|
186
|
+
callback: (event: Event) => void,
|
187
|
+
delay?: number
|
188
|
+
): string {
|
189
|
+
const operationId = this.generateOperationId(event, element);
|
190
|
+
const debounceDelay = delay || this.config.debounceDelay;
|
191
|
+
|
192
|
+
const debouncedCallback = (event: Event) => {
|
193
|
+
this.debounce(operationId, () => {
|
194
|
+
this.addOperation({
|
195
|
+
id: `${operationId}-${Date.now()}`,
|
196
|
+
element,
|
197
|
+
callback: () => callback(event),
|
198
|
+
priority: 'normal',
|
199
|
+
timestamp: performance.now(),
|
200
|
+
cost: this.estimateOperationCost(event.type)
|
201
|
+
});
|
202
|
+
}, debounceDelay);
|
203
|
+
};
|
204
|
+
|
205
|
+
const options = this.config.passiveListeners ? { passive: true } : false;
|
206
|
+
element.addEventListener(event, debouncedCallback, options);
|
207
|
+
|
208
|
+
return operationId;
|
209
|
+
}
|
210
|
+
|
211
|
+
/**
|
212
|
+
* Add throttled event listener
|
213
|
+
*/
|
214
|
+
public addThrottledListener(
|
215
|
+
element: Element,
|
216
|
+
event: string,
|
217
|
+
callback: (event: Event) => void,
|
218
|
+
delay?: number
|
219
|
+
): string {
|
220
|
+
const operationId = this.generateOperationId(event, element);
|
221
|
+
const throttleDelay = delay || this.config.throttleDelay;
|
222
|
+
|
223
|
+
const throttledCallback = (event: Event) => {
|
224
|
+
this.throttle(operationId, () => {
|
225
|
+
this.addOperation({
|
226
|
+
id: `${operationId}-${Date.now()}`,
|
227
|
+
element,
|
228
|
+
callback: () => callback(event),
|
229
|
+
priority: 'normal',
|
230
|
+
timestamp: performance.now(),
|
231
|
+
cost: this.estimateOperationCost(event.type)
|
232
|
+
});
|
233
|
+
}, throttleDelay);
|
234
|
+
};
|
235
|
+
|
236
|
+
const options = this.config.passiveListeners ? { passive: true } : false;
|
237
|
+
element.addEventListener(event, throttledCallback, options);
|
238
|
+
|
239
|
+
return operationId;
|
240
|
+
}
|
241
|
+
|
242
|
+
/**
|
243
|
+
* Add passive event listener
|
244
|
+
*/
|
245
|
+
public addPassiveListener(
|
246
|
+
element: Element,
|
247
|
+
event: string,
|
248
|
+
callback: (event: Event) => void
|
249
|
+
): string {
|
250
|
+
const operationId = this.generateOperationId(event, element);
|
251
|
+
|
252
|
+
const passiveCallback = (event: Event) => {
|
253
|
+
this.addOperation({
|
254
|
+
id: `${operationId}-${Date.now()}`,
|
255
|
+
element,
|
256
|
+
callback: () => callback(event),
|
257
|
+
priority: 'normal',
|
258
|
+
timestamp: performance.now(),
|
259
|
+
cost: this.estimateOperationCost(event.type)
|
260
|
+
});
|
261
|
+
};
|
262
|
+
|
263
|
+
element.addEventListener(event, passiveCallback, { passive: true });
|
264
|
+
|
265
|
+
return operationId;
|
266
|
+
}
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Batch DOM operations for better performance
|
270
|
+
*/
|
271
|
+
public batchDOMOperations(operations: () => void): void {
|
272
|
+
// Use requestAnimationFrame to batch DOM operations
|
273
|
+
requestAnimationFrame(() => {
|
274
|
+
operations();
|
275
|
+
});
|
276
|
+
}
|
277
|
+
|
278
|
+
/**
|
279
|
+
* Remove passive event listener
|
280
|
+
*/
|
281
|
+
public removePassiveListener(_element: Element, _event: string, operationId: string): void {
|
282
|
+
this.removeOperation(operationId);
|
283
|
+
// Note: In a real implementation, we'd need to store the callback reference
|
284
|
+
// to properly remove it. For now, we just remove from our operation queue.
|
285
|
+
}
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Add delegated event listener
|
289
|
+
*/
|
290
|
+
public addDelegatedListener(
|
291
|
+
container: Element,
|
292
|
+
selector: string,
|
293
|
+
event: string,
|
294
|
+
callback: (event: Event, target: Element) => void
|
295
|
+
): string {
|
296
|
+
const operationId = this.generateOperationId(`delegated-${event}`, container);
|
297
|
+
|
298
|
+
const delegatedCallback = (event: Event) => {
|
299
|
+
const target = event.target as Element;
|
300
|
+
if (target && target.matches && target.matches(selector)) {
|
301
|
+
this.addOperation({
|
302
|
+
id: `${operationId}-${Date.now()}`,
|
303
|
+
element: container,
|
304
|
+
callback: () => callback(event, target),
|
305
|
+
priority: 'normal',
|
306
|
+
timestamp: performance.now(),
|
307
|
+
cost: this.estimateOperationCost(event.type)
|
308
|
+
});
|
309
|
+
}
|
310
|
+
};
|
311
|
+
|
312
|
+
const options = this.config.passiveListeners ? { passive: true } : false;
|
313
|
+
container.addEventListener(event, delegatedCallback, options);
|
314
|
+
|
315
|
+
return operationId;
|
316
|
+
}
|
317
|
+
|
318
|
+
/**
|
319
|
+
* Remove operation from queue
|
320
|
+
*/
|
321
|
+
public removeOperation(operationId: string): void {
|
322
|
+
this.operationQueue.delete(operationId);
|
323
|
+
this.batchQueue = this.batchQueue.filter(op => op.id !== operationId);
|
324
|
+
}
|
325
|
+
|
326
|
+
/**
|
327
|
+
* Get current performance metrics
|
328
|
+
*/
|
329
|
+
public getMetrics(): PerformanceMetrics {
|
330
|
+
return { ...this.metrics };
|
331
|
+
}
|
332
|
+
|
333
|
+
/**
|
334
|
+
* Force process all queued operations
|
335
|
+
*/
|
336
|
+
public flush(): void {
|
337
|
+
this.processOperations(true);
|
338
|
+
}
|
339
|
+
|
340
|
+
/**
|
341
|
+
* Setup ResizeObserver with batching
|
342
|
+
*/
|
343
|
+
private setupResizeObserver(): void {
|
344
|
+
if (!window.ResizeObserver) {
|
345
|
+
console.warn('ResizeObserver not supported, falling back to window resize');
|
346
|
+
this.setupFallbackResize();
|
347
|
+
return;
|
348
|
+
}
|
349
|
+
|
350
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
351
|
+
if (this.config.batchOperations) {
|
352
|
+
// Batch resize operations
|
353
|
+
entries.forEach(entry => {
|
354
|
+
const operationId = this.generateOperationId('resize', entry.target);
|
355
|
+
const operation = this.operationQueue.get(operationId);
|
356
|
+
|
357
|
+
if (operation) {
|
358
|
+
operation.timestamp = performance.now();
|
359
|
+
this.batchQueue.push(operation);
|
360
|
+
}
|
361
|
+
});
|
362
|
+
} else {
|
363
|
+
// Process immediately
|
364
|
+
entries.forEach(entry => {
|
365
|
+
const operationId = this.generateOperationId('resize', entry.target);
|
366
|
+
const operation = this.operationQueue.get(operationId);
|
367
|
+
|
368
|
+
if (operation) {
|
369
|
+
operation.callback();
|
370
|
+
}
|
371
|
+
});
|
372
|
+
}
|
373
|
+
});
|
374
|
+
}
|
375
|
+
|
376
|
+
/**
|
377
|
+
* Setup IntersectionObserver for visibility-based activation
|
378
|
+
*/
|
379
|
+
private setupIntersectionObserver(): void {
|
380
|
+
if (!window.IntersectionObserver) {
|
381
|
+
console.warn('IntersectionObserver not supported');
|
382
|
+
return;
|
383
|
+
}
|
384
|
+
|
385
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
386
|
+
entries.forEach(entry => {
|
387
|
+
if (entry.isIntersecting) {
|
388
|
+
this.visibleElements.add(entry.target);
|
389
|
+
} else {
|
390
|
+
this.visibleElements.delete(entry.target);
|
391
|
+
}
|
392
|
+
|
393
|
+
const operationId = this.generateOperationId('intersection', entry.target);
|
394
|
+
const operation = this.operationQueue.get(operationId);
|
395
|
+
|
396
|
+
if (operation) {
|
397
|
+
if (this.config.batchOperations) {
|
398
|
+
operation.timestamp = performance.now();
|
399
|
+
this.batchQueue.push(operation);
|
400
|
+
} else {
|
401
|
+
operation.callback();
|
402
|
+
}
|
403
|
+
}
|
404
|
+
});
|
405
|
+
}, {
|
406
|
+
rootMargin: '50px', // Start processing slightly before element is visible
|
407
|
+
threshold: [0, 0.1, 0.5, 1.0]
|
408
|
+
});
|
409
|
+
}
|
410
|
+
|
411
|
+
/**
|
412
|
+
* Setup fallback resize handling for older browsers
|
413
|
+
*/
|
414
|
+
private setupFallbackResize(): void {
|
415
|
+
let resizeTimeout: number;
|
416
|
+
|
417
|
+
const handleResize = () => {
|
418
|
+
clearTimeout(resizeTimeout);
|
419
|
+
resizeTimeout = window.setTimeout(() => {
|
420
|
+
// Process all resize operations
|
421
|
+
this.operationQueue.forEach(operation => {
|
422
|
+
if (operation.id.includes('resize')) {
|
423
|
+
if (this.config.batchOperations) {
|
424
|
+
this.batchQueue.push(operation);
|
425
|
+
} else {
|
426
|
+
operation.callback();
|
427
|
+
}
|
428
|
+
}
|
429
|
+
});
|
430
|
+
}, this.config.debounceDelay);
|
431
|
+
};
|
432
|
+
|
433
|
+
const options = this.config.passiveListeners ? { passive: true } : false;
|
434
|
+
window.addEventListener('resize', handleResize, options);
|
435
|
+
}
|
436
|
+
|
437
|
+
/**
|
438
|
+
* Start the main processing loop
|
439
|
+
*/
|
440
|
+
private startProcessingLoop(): void {
|
441
|
+
if (this.config.useRAF) {
|
442
|
+
this.processWithRAF();
|
443
|
+
} else {
|
444
|
+
this.processWithTimer();
|
445
|
+
}
|
446
|
+
}
|
447
|
+
|
448
|
+
/**
|
449
|
+
* Process operations using requestAnimationFrame
|
450
|
+
*/
|
451
|
+
private processWithRAF(): void {
|
452
|
+
const process = (timestamp: number) => {
|
453
|
+
this.updateMetrics(timestamp);
|
454
|
+
this.processOperations();
|
455
|
+
|
456
|
+
if (!this.isProcessing) {
|
457
|
+
this.rafId = requestAnimationFrame(process);
|
458
|
+
}
|
459
|
+
};
|
460
|
+
|
461
|
+
this.rafId = requestAnimationFrame(process);
|
462
|
+
}
|
463
|
+
|
464
|
+
/**
|
465
|
+
* Process operations using timer (fallback)
|
466
|
+
*/
|
467
|
+
private processWithTimer(): void {
|
468
|
+
const interval = Math.max(this.config.performanceTarget, 16);
|
469
|
+
|
470
|
+
const process = () => {
|
471
|
+
if (!this.isProcessing) {
|
472
|
+
this.processOperations();
|
473
|
+
setTimeout(process, interval);
|
474
|
+
}
|
475
|
+
};
|
476
|
+
|
477
|
+
setTimeout(process, interval);
|
478
|
+
}
|
479
|
+
|
480
|
+
/**
|
481
|
+
* Process queued operations with performance budgeting
|
482
|
+
*/
|
483
|
+
private processOperations(forceFlush: boolean = false): void {
|
484
|
+
if (this.batchQueue.length === 0 && this.operationQueue.size === 0) {
|
485
|
+
return;
|
486
|
+
}
|
487
|
+
|
488
|
+
const startTime = performance.now();
|
489
|
+
const budget = forceFlush ? Infinity : this.config.performanceTarget;
|
490
|
+
let processedCount = 0;
|
491
|
+
|
492
|
+
// Sort operations by priority and timestamp
|
493
|
+
const sortedOperations = this.getSortedOperations();
|
494
|
+
|
495
|
+
for (const operation of sortedOperations) {
|
496
|
+
const elapsed = performance.now() - startTime;
|
497
|
+
|
498
|
+
// Check if we have budget remaining
|
499
|
+
if (!forceFlush && elapsed + operation.cost > budget) {
|
500
|
+
break;
|
501
|
+
}
|
502
|
+
|
503
|
+
// Only process operations for visible elements (if intersection-based)
|
504
|
+
if (this.config.intersectionBased &&
|
505
|
+
!this.visibleElements.has(operation.element) &&
|
506
|
+
operation.priority !== 'high') {
|
507
|
+
continue;
|
508
|
+
}
|
509
|
+
|
510
|
+
try {
|
511
|
+
operation.callback();
|
512
|
+
processedCount++;
|
513
|
+
|
514
|
+
// Remove from queues
|
515
|
+
this.operationQueue.delete(operation.id);
|
516
|
+
this.batchQueue = this.batchQueue.filter(op => op.id !== operation.id);
|
517
|
+
|
518
|
+
// Respect batch size limit
|
519
|
+
if (!forceFlush && processedCount >= this.config.maxBatchSize) {
|
520
|
+
break;
|
521
|
+
}
|
522
|
+
} catch (error) {
|
523
|
+
console.error('Error processing operation:', error);
|
524
|
+
this.operationQueue.delete(operation.id);
|
525
|
+
}
|
526
|
+
}
|
527
|
+
|
528
|
+
// Update metrics
|
529
|
+
this.metrics.operationsPerFrame = processedCount;
|
530
|
+
this.metrics.queueSize = this.operationQueue.size + this.batchQueue.length;
|
531
|
+
}
|
532
|
+
|
533
|
+
/**
|
534
|
+
* Get operations sorted by priority and age
|
535
|
+
*/
|
536
|
+
private getSortedOperations(): EventOperation[] {
|
537
|
+
const allOperations = [
|
538
|
+
...Array.from(this.operationQueue.values()),
|
539
|
+
...this.batchQueue
|
540
|
+
];
|
541
|
+
|
542
|
+
return allOperations.sort((a, b) => {
|
543
|
+
// Priority first
|
544
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
545
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
546
|
+
|
547
|
+
if (priorityDiff !== 0) {
|
548
|
+
return priorityDiff;
|
549
|
+
}
|
550
|
+
|
551
|
+
// Then by timestamp (older first)
|
552
|
+
return a.timestamp - b.timestamp;
|
553
|
+
});
|
554
|
+
}
|
555
|
+
|
556
|
+
/**
|
557
|
+
* Add operation to queue
|
558
|
+
*/
|
559
|
+
private addOperation(operation: EventOperation): void {
|
560
|
+
this.operationQueue.set(operation.id, operation);
|
561
|
+
}
|
562
|
+
|
563
|
+
/**
|
564
|
+
* Debounce function execution
|
565
|
+
*/
|
566
|
+
private debounce(key: string, func: () => void, delay: number): void {
|
567
|
+
const existingTimer = this.debounceTimers.get(key);
|
568
|
+
if (existingTimer) {
|
569
|
+
clearTimeout(existingTimer);
|
570
|
+
}
|
571
|
+
|
572
|
+
const timer = window.setTimeout(() => {
|
573
|
+
func();
|
574
|
+
this.debounceTimers.delete(key);
|
575
|
+
}, delay);
|
576
|
+
|
577
|
+
this.debounceTimers.set(key, timer);
|
578
|
+
}
|
579
|
+
|
580
|
+
/**
|
581
|
+
* Throttle function execution
|
582
|
+
*/
|
583
|
+
private throttle(key: string, func: () => void, delay: number): void {
|
584
|
+
const lastTime = this.lastThrottleTime.get(key) || 0;
|
585
|
+
const now = performance.now();
|
586
|
+
|
587
|
+
if (now - lastTime >= delay) {
|
588
|
+
func();
|
589
|
+
this.lastThrottleTime.set(key, now);
|
590
|
+
} else {
|
591
|
+
// Schedule for later if not already scheduled
|
592
|
+
if (!this.throttleTimers.has(key)) {
|
593
|
+
const timer = window.setTimeout(() => {
|
594
|
+
func();
|
595
|
+
this.lastThrottleTime.set(key, performance.now());
|
596
|
+
this.throttleTimers.delete(key);
|
597
|
+
}, delay - (now - lastTime));
|
598
|
+
|
599
|
+
this.throttleTimers.set(key, timer);
|
600
|
+
}
|
601
|
+
}
|
602
|
+
}
|
603
|
+
|
604
|
+
/**
|
605
|
+
* Generate unique operation ID
|
606
|
+
*/
|
607
|
+
private generateOperationId(type: string, element: Element): string {
|
608
|
+
const elementId = element.id || element.tagName + Math.random().toString(36).substring(2, 11);
|
609
|
+
return `${type}-${elementId}`;
|
610
|
+
}
|
611
|
+
|
612
|
+
/**
|
613
|
+
* Estimate operation execution cost
|
614
|
+
*/
|
615
|
+
private estimateOperationCost(operationType: string): number {
|
616
|
+
const baseCosts = {
|
617
|
+
resize: 2,
|
618
|
+
intersection: 1,
|
619
|
+
scroll: 1,
|
620
|
+
click: 0.5,
|
621
|
+
mousemove: 0.5,
|
622
|
+
default: 1
|
623
|
+
};
|
624
|
+
|
625
|
+
return baseCosts[operationType as keyof typeof baseCosts] || baseCosts.default;
|
626
|
+
}
|
627
|
+
|
628
|
+
/**
|
629
|
+
* Calculate intersection ratio
|
630
|
+
*/
|
631
|
+
private calculateIntersectionRatio(rect: DOMRect, rootBounds: DOMRect): number {
|
632
|
+
const intersectionArea = Math.max(0,
|
633
|
+
Math.min(rect.right, rootBounds.right) - Math.max(rect.left, rootBounds.left)
|
634
|
+
) * Math.max(0,
|
635
|
+
Math.min(rect.bottom, rootBounds.bottom) - Math.max(rect.top, rootBounds.top)
|
636
|
+
);
|
637
|
+
|
638
|
+
const elementArea = rect.width * rect.height;
|
639
|
+
return elementArea > 0 ? intersectionArea / elementArea : 0;
|
640
|
+
}
|
641
|
+
|
642
|
+
/**
|
643
|
+
* Check if element is intersecting viewport
|
644
|
+
*/
|
645
|
+
private isElementIntersecting(rect: DOMRect, rootBounds: DOMRect): boolean {
|
646
|
+
return rect.left < rootBounds.right &&
|
647
|
+
rect.right > rootBounds.left &&
|
648
|
+
rect.top < rootBounds.bottom &&
|
649
|
+
rect.bottom > rootBounds.top;
|
650
|
+
}
|
651
|
+
|
652
|
+
/**
|
653
|
+
* Setup performance monitoring
|
654
|
+
*/
|
655
|
+
private setupPerformanceMonitoring(): void {
|
656
|
+
this.lastFrameTime = performance.now();
|
657
|
+
}
|
658
|
+
|
659
|
+
/**
|
660
|
+
* Update performance metrics
|
661
|
+
*/
|
662
|
+
private updateMetrics(timestamp: number): void {
|
663
|
+
const deltaTime = timestamp - this.lastFrameTime;
|
664
|
+
this.frameCount++;
|
665
|
+
|
666
|
+
// Update frame time and FPS
|
667
|
+
this.metrics.frameTime = deltaTime;
|
668
|
+
this.metrics.fps = 1000 / deltaTime;
|
669
|
+
|
670
|
+
// Track dropped frames (frames taking longer than target)
|
671
|
+
if (deltaTime > this.config.performanceTarget * 1.5) {
|
672
|
+
this.metrics.droppedFrames++;
|
673
|
+
}
|
674
|
+
|
675
|
+
// Calculate average latency (simplified)
|
676
|
+
const currentLatency = this.batchQueue.length > 0
|
677
|
+
? timestamp - Math.min(...this.batchQueue.map(op => op.timestamp))
|
678
|
+
: 0;
|
679
|
+
|
680
|
+
this.metrics.averageLatency = (this.metrics.averageLatency + currentLatency) / 2;
|
681
|
+
|
682
|
+
this.lastFrameTime = timestamp;
|
683
|
+
}
|
684
|
+
|
685
|
+
/**
|
686
|
+
* Stop processing loop
|
687
|
+
*/
|
688
|
+
private stopProcessingLoop(): void {
|
689
|
+
this.isProcessing = true;
|
690
|
+
|
691
|
+
if (this.rafId) {
|
692
|
+
cancelAnimationFrame(this.rafId);
|
693
|
+
this.rafId = null;
|
694
|
+
}
|
695
|
+
}
|
696
|
+
|
697
|
+
/**
|
698
|
+
* Clean up observers
|
699
|
+
*/
|
700
|
+
private cleanupObservers(): void {
|
701
|
+
if (this.resizeObserver) {
|
702
|
+
this.resizeObserver.disconnect();
|
703
|
+
this.resizeObserver = null;
|
704
|
+
}
|
705
|
+
|
706
|
+
if (this.intersectionObserver) {
|
707
|
+
this.intersectionObserver.disconnect();
|
708
|
+
this.intersectionObserver = null;
|
709
|
+
}
|
710
|
+
}
|
711
|
+
|
712
|
+
/**
|
713
|
+
* Clear all queues
|
714
|
+
*/
|
715
|
+
private clearQueues(): void {
|
716
|
+
this.operationQueue.clear();
|
717
|
+
this.batchQueue = [];
|
718
|
+
|
719
|
+
// Clear timers
|
720
|
+
this.debounceTimers.forEach(timer => clearTimeout(timer));
|
721
|
+
this.throttleTimers.forEach(timer => clearTimeout(timer));
|
722
|
+
this.debounceTimers.clear();
|
723
|
+
this.throttleTimers.clear();
|
724
|
+
this.lastThrottleTime.clear();
|
725
|
+
}
|
726
|
+
|
727
|
+
/**
|
728
|
+
* Create initial metrics
|
729
|
+
*/
|
730
|
+
private createInitialMetrics(): PerformanceMetrics {
|
731
|
+
return {
|
732
|
+
frameTime: 0,
|
733
|
+
fps: 60,
|
734
|
+
operationsPerFrame: 0,
|
735
|
+
queueSize: 0,
|
736
|
+
droppedFrames: 0,
|
737
|
+
averageLatency: 0
|
738
|
+
};
|
739
|
+
}
|
740
|
+
}
|