@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,179 @@
|
|
1
|
+
/**
|
2
|
+
* ResizeObserver Polyfill for ProteusJS
|
3
|
+
* Provides ResizeObserver functionality for browsers that don't support it
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface ResizeObserverEntry {
|
7
|
+
target: Element;
|
8
|
+
contentRect: DOMRectReadOnly;
|
9
|
+
borderBoxSize?: ResizeObserverSize[];
|
10
|
+
contentBoxSize?: ResizeObserverSize[];
|
11
|
+
devicePixelContentBoxSize?: ResizeObserverSize[];
|
12
|
+
}
|
13
|
+
|
14
|
+
export interface ResizeObserverSize {
|
15
|
+
inlineSize: number;
|
16
|
+
blockSize: number;
|
17
|
+
}
|
18
|
+
|
19
|
+
export type ResizeObserverCallback = (entries: ResizeObserverEntry[]) => void;
|
20
|
+
|
21
|
+
export class ResizeObserverPolyfill {
|
22
|
+
private callback: ResizeObserverCallback;
|
23
|
+
private observedElements: Map<Element, { lastWidth: number; lastHeight: number }> = new Map();
|
24
|
+
private rafId: number | null = null;
|
25
|
+
private isObserving: boolean = false;
|
26
|
+
|
27
|
+
constructor(callback: ResizeObserverCallback) {
|
28
|
+
this.callback = callback;
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* Start observing an element for resize changes
|
33
|
+
*/
|
34
|
+
public observe(element: Element, options?: ResizeObserverOptions): void {
|
35
|
+
if (this.observedElements.has(element)) return;
|
36
|
+
|
37
|
+
const rect = element.getBoundingClientRect();
|
38
|
+
this.observedElements.set(element, {
|
39
|
+
lastWidth: rect.width,
|
40
|
+
lastHeight: rect.height
|
41
|
+
});
|
42
|
+
|
43
|
+
if (!this.isObserving) {
|
44
|
+
this.startObserving();
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Stop observing an element
|
50
|
+
*/
|
51
|
+
public unobserve(element: Element): void {
|
52
|
+
this.observedElements.delete(element);
|
53
|
+
|
54
|
+
if (this.observedElements.size === 0) {
|
55
|
+
this.stopObserving();
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Disconnect all observations
|
61
|
+
*/
|
62
|
+
public disconnect(): void {
|
63
|
+
this.observedElements.clear();
|
64
|
+
this.stopObserving();
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Start the polling mechanism
|
69
|
+
*/
|
70
|
+
private startObserving(): void {
|
71
|
+
if (this.isObserving) return;
|
72
|
+
|
73
|
+
this.isObserving = true;
|
74
|
+
this.checkForChanges();
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* Stop the polling mechanism
|
79
|
+
*/
|
80
|
+
private stopObserving(): void {
|
81
|
+
if (!this.isObserving) return;
|
82
|
+
|
83
|
+
this.isObserving = false;
|
84
|
+
if (this.rafId) {
|
85
|
+
cancelAnimationFrame(this.rafId);
|
86
|
+
this.rafId = null;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Check for size changes in observed elements
|
92
|
+
*/
|
93
|
+
private checkForChanges(): void {
|
94
|
+
if (!this.isObserving) return;
|
95
|
+
|
96
|
+
const changedEntries: ResizeObserverEntry[] = [];
|
97
|
+
|
98
|
+
this.observedElements.forEach((lastSize, element) => {
|
99
|
+
// Check if element is still in DOM
|
100
|
+
if (!document.contains(element)) {
|
101
|
+
this.observedElements.delete(element);
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
|
105
|
+
const rect = element.getBoundingClientRect();
|
106
|
+
const currentWidth = rect.width;
|
107
|
+
const currentHeight = rect.height;
|
108
|
+
|
109
|
+
if (currentWidth !== lastSize.lastWidth || currentHeight !== lastSize.lastHeight) {
|
110
|
+
// Update stored size
|
111
|
+
this.observedElements.set(element, {
|
112
|
+
lastWidth: currentWidth,
|
113
|
+
lastHeight: currentHeight
|
114
|
+
});
|
115
|
+
|
116
|
+
// Create entry
|
117
|
+
const entry: ResizeObserverEntry = {
|
118
|
+
target: element,
|
119
|
+
contentRect: this.createDOMRectReadOnly(rect),
|
120
|
+
contentBoxSize: [{
|
121
|
+
inlineSize: currentWidth,
|
122
|
+
blockSize: currentHeight
|
123
|
+
}],
|
124
|
+
borderBoxSize: [{
|
125
|
+
inlineSize: currentWidth,
|
126
|
+
blockSize: currentHeight
|
127
|
+
}]
|
128
|
+
};
|
129
|
+
|
130
|
+
changedEntries.push(entry);
|
131
|
+
}
|
132
|
+
});
|
133
|
+
|
134
|
+
// Call callback if there are changes
|
135
|
+
if (changedEntries.length > 0) {
|
136
|
+
try {
|
137
|
+
this.callback(changedEntries);
|
138
|
+
} catch (error) {
|
139
|
+
console.error('ResizeObserver callback error:', error);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
// Schedule next check
|
144
|
+
if (this.isObserving) {
|
145
|
+
this.rafId = requestAnimationFrame(() => this.checkForChanges());
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Create a DOMRectReadOnly-like object
|
151
|
+
*/
|
152
|
+
private createDOMRectReadOnly(rect: DOMRect): DOMRectReadOnly {
|
153
|
+
return {
|
154
|
+
x: rect.x,
|
155
|
+
y: rect.y,
|
156
|
+
width: rect.width,
|
157
|
+
height: rect.height,
|
158
|
+
top: rect.top,
|
159
|
+
right: rect.right,
|
160
|
+
bottom: rect.bottom,
|
161
|
+
left: rect.left,
|
162
|
+
toJSON: () => ({
|
163
|
+
x: rect.x,
|
164
|
+
y: rect.y,
|
165
|
+
width: rect.width,
|
166
|
+
height: rect.height,
|
167
|
+
top: rect.top,
|
168
|
+
right: rect.right,
|
169
|
+
bottom: rect.bottom,
|
170
|
+
left: rect.left
|
171
|
+
})
|
172
|
+
};
|
173
|
+
}
|
174
|
+
}
|
175
|
+
|
176
|
+
// Add static method for feature detection
|
177
|
+
(ResizeObserverPolyfill as any).isSupported = (): boolean => {
|
178
|
+
return typeof ResizeObserver !== 'undefined';
|
179
|
+
};
|
@@ -0,0 +1,519 @@
|
|
1
|
+
/**
|
2
|
+
* Batch DOM Operations for ProteusJS
|
3
|
+
* Efficient DOM manipulation with read/write separation and layout thrashing prevention
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface DOMOperation {
|
7
|
+
id: string;
|
8
|
+
type: 'read' | 'write';
|
9
|
+
element: Element;
|
10
|
+
operation: () => any;
|
11
|
+
priority: 'high' | 'normal' | 'low';
|
12
|
+
timestamp: number;
|
13
|
+
dependencies?: string[];
|
14
|
+
}
|
15
|
+
|
16
|
+
export interface BatchConfig {
|
17
|
+
maxBatchSize: number;
|
18
|
+
frameTimeLimit: number;
|
19
|
+
separateReadWrite: boolean;
|
20
|
+
measurePerformance: boolean;
|
21
|
+
autoFlush: boolean;
|
22
|
+
flushInterval: number;
|
23
|
+
}
|
24
|
+
|
25
|
+
export interface BatchMetrics {
|
26
|
+
totalOperations: number;
|
27
|
+
readOperations: number;
|
28
|
+
writeOperations: number;
|
29
|
+
batchesProcessed: number;
|
30
|
+
averageBatchTime: number;
|
31
|
+
layoutThrashes: number;
|
32
|
+
preventedThrashes: number;
|
33
|
+
}
|
34
|
+
|
35
|
+
export class BatchDOMOperations {
|
36
|
+
private config: Required<BatchConfig>;
|
37
|
+
private readQueue: DOMOperation[] = [];
|
38
|
+
private writeQueue: DOMOperation[] = [];
|
39
|
+
private processingQueue: DOMOperation[] = [];
|
40
|
+
private metrics: BatchMetrics;
|
41
|
+
private rafId: number | null = null;
|
42
|
+
private flushTimer: number | null = null;
|
43
|
+
private isProcessing: boolean = false;
|
44
|
+
private operationResults: Map<string, any> = new Map();
|
45
|
+
|
46
|
+
constructor(config: Partial<BatchConfig> = {}) {
|
47
|
+
this.config = {
|
48
|
+
maxBatchSize: 50,
|
49
|
+
frameTimeLimit: 16, // 60fps target
|
50
|
+
separateReadWrite: true,
|
51
|
+
measurePerformance: true,
|
52
|
+
autoFlush: true,
|
53
|
+
flushInterval: 100,
|
54
|
+
...config
|
55
|
+
};
|
56
|
+
|
57
|
+
this.metrics = this.createInitialMetrics();
|
58
|
+
|
59
|
+
if (this.config.autoFlush) {
|
60
|
+
this.startAutoFlush();
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Queue a DOM read operation
|
66
|
+
*/
|
67
|
+
public queueRead<T>(
|
68
|
+
element: Element,
|
69
|
+
operation: () => T,
|
70
|
+
priority: 'high' | 'normal' | 'low' = 'normal',
|
71
|
+
dependencies: string[] = []
|
72
|
+
): Promise<T> {
|
73
|
+
const id = this.generateOperationId('read');
|
74
|
+
|
75
|
+
return new Promise((resolve, reject) => {
|
76
|
+
const domOperation: DOMOperation = {
|
77
|
+
id,
|
78
|
+
type: 'read',
|
79
|
+
element,
|
80
|
+
operation: () => {
|
81
|
+
try {
|
82
|
+
const result = operation();
|
83
|
+
this.operationResults.set(id, result);
|
84
|
+
resolve(result);
|
85
|
+
return result;
|
86
|
+
} catch (error) {
|
87
|
+
reject(error);
|
88
|
+
throw error;
|
89
|
+
}
|
90
|
+
},
|
91
|
+
priority,
|
92
|
+
timestamp: performance.now(),
|
93
|
+
dependencies
|
94
|
+
};
|
95
|
+
|
96
|
+
this.readQueue.push(domOperation);
|
97
|
+
this.scheduleProcessing();
|
98
|
+
});
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Queue a DOM write operation
|
103
|
+
*/
|
104
|
+
public queueWrite(
|
105
|
+
element: Element,
|
106
|
+
operation: () => void,
|
107
|
+
priority: 'high' | 'normal' | 'low' = 'normal',
|
108
|
+
dependencies: string[] = []
|
109
|
+
): Promise<void> {
|
110
|
+
const id = this.generateOperationId('write');
|
111
|
+
|
112
|
+
return new Promise((resolve, reject) => {
|
113
|
+
const domOperation: DOMOperation = {
|
114
|
+
id,
|
115
|
+
type: 'write',
|
116
|
+
element,
|
117
|
+
operation: () => {
|
118
|
+
try {
|
119
|
+
operation();
|
120
|
+
resolve();
|
121
|
+
} catch (error) {
|
122
|
+
reject(error);
|
123
|
+
throw error;
|
124
|
+
}
|
125
|
+
},
|
126
|
+
priority,
|
127
|
+
timestamp: performance.now(),
|
128
|
+
dependencies
|
129
|
+
};
|
130
|
+
|
131
|
+
this.writeQueue.push(domOperation);
|
132
|
+
this.scheduleProcessing();
|
133
|
+
});
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* Batch multiple style changes
|
138
|
+
*/
|
139
|
+
public batchStyles(
|
140
|
+
element: Element,
|
141
|
+
styles: Record<string, string>,
|
142
|
+
priority: 'high' | 'normal' | 'low' = 'normal'
|
143
|
+
): Promise<void> {
|
144
|
+
return this.queueWrite(
|
145
|
+
element,
|
146
|
+
() => {
|
147
|
+
const htmlElement = element as HTMLElement;
|
148
|
+
Object.entries(styles).forEach(([property, value]) => {
|
149
|
+
htmlElement.style.setProperty(property, value);
|
150
|
+
});
|
151
|
+
},
|
152
|
+
priority
|
153
|
+
);
|
154
|
+
}
|
155
|
+
|
156
|
+
/**
|
157
|
+
* Batch multiple class changes
|
158
|
+
*/
|
159
|
+
public batchClasses(
|
160
|
+
element: Element,
|
161
|
+
changes: { add?: string[]; remove?: string[]; toggle?: string[] },
|
162
|
+
priority: 'high' | 'normal' | 'low' = 'normal'
|
163
|
+
): Promise<void> {
|
164
|
+
return this.queueWrite(
|
165
|
+
element,
|
166
|
+
() => {
|
167
|
+
if (changes.add) {
|
168
|
+
element.classList.add(...changes.add);
|
169
|
+
}
|
170
|
+
if (changes.remove) {
|
171
|
+
element.classList.remove(...changes.remove);
|
172
|
+
}
|
173
|
+
if (changes.toggle) {
|
174
|
+
changes.toggle.forEach(className => {
|
175
|
+
element.classList.toggle(className);
|
176
|
+
});
|
177
|
+
}
|
178
|
+
},
|
179
|
+
priority
|
180
|
+
);
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Batch multiple attribute changes
|
185
|
+
*/
|
186
|
+
public batchAttributes(
|
187
|
+
element: Element,
|
188
|
+
attributes: Record<string, string | null>,
|
189
|
+
priority: 'high' | 'normal' | 'low' = 'normal'
|
190
|
+
): Promise<void> {
|
191
|
+
return this.queueWrite(
|
192
|
+
element,
|
193
|
+
() => {
|
194
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
195
|
+
if (value === null) {
|
196
|
+
element.removeAttribute(name);
|
197
|
+
} else {
|
198
|
+
element.setAttribute(name, value);
|
199
|
+
}
|
200
|
+
});
|
201
|
+
},
|
202
|
+
priority
|
203
|
+
);
|
204
|
+
}
|
205
|
+
|
206
|
+
/**
|
207
|
+
* Read multiple properties efficiently
|
208
|
+
*/
|
209
|
+
public batchReads<T extends Record<string, () => any>>(
|
210
|
+
element: Element,
|
211
|
+
readers: T,
|
212
|
+
priority: 'high' | 'normal' | 'low' = 'normal'
|
213
|
+
): Promise<{ [K in keyof T]: ReturnType<T[K]> }> {
|
214
|
+
return this.queueRead(
|
215
|
+
element,
|
216
|
+
() => {
|
217
|
+
const results = {} as { [K in keyof T]: ReturnType<T[K]> };
|
218
|
+
Object.entries(readers).forEach(([key, reader]) => {
|
219
|
+
results[key as keyof T] = (reader as () => any)();
|
220
|
+
});
|
221
|
+
return results;
|
222
|
+
},
|
223
|
+
priority
|
224
|
+
);
|
225
|
+
}
|
226
|
+
|
227
|
+
/**
|
228
|
+
* Measure element dimensions efficiently
|
229
|
+
*/
|
230
|
+
public measureElement(
|
231
|
+
element: Element,
|
232
|
+
measurements: ('width' | 'height' | 'top' | 'left' | 'right' | 'bottom')[] = ['width', 'height'],
|
233
|
+
priority: 'high' | 'normal' | 'low' = 'normal'
|
234
|
+
): Promise<Partial<DOMRect>> {
|
235
|
+
return this.queueRead(
|
236
|
+
element,
|
237
|
+
() => {
|
238
|
+
const rect = element.getBoundingClientRect();
|
239
|
+
const result: Partial<DOMRect> = {};
|
240
|
+
|
241
|
+
measurements.forEach(measurement => {
|
242
|
+
(result as any)[measurement] = rect[measurement as keyof DOMRect];
|
243
|
+
});
|
244
|
+
|
245
|
+
return result;
|
246
|
+
},
|
247
|
+
priority
|
248
|
+
);
|
249
|
+
}
|
250
|
+
|
251
|
+
/**
|
252
|
+
* Force flush all queued operations
|
253
|
+
*/
|
254
|
+
public flush(): Promise<void> {
|
255
|
+
return new Promise((resolve) => {
|
256
|
+
this.processOperations(true).then(() => {
|
257
|
+
resolve();
|
258
|
+
});
|
259
|
+
});
|
260
|
+
}
|
261
|
+
|
262
|
+
/**
|
263
|
+
* Get current batch metrics
|
264
|
+
*/
|
265
|
+
public getMetrics(): BatchMetrics {
|
266
|
+
return { ...this.metrics };
|
267
|
+
}
|
268
|
+
|
269
|
+
/**
|
270
|
+
* Clear all queues
|
271
|
+
*/
|
272
|
+
public clear(): void {
|
273
|
+
this.readQueue = [];
|
274
|
+
this.writeQueue = [];
|
275
|
+
this.processingQueue = [];
|
276
|
+
this.operationResults.clear();
|
277
|
+
}
|
278
|
+
|
279
|
+
/**
|
280
|
+
* Destroy the batch processor
|
281
|
+
*/
|
282
|
+
public destroy(): void {
|
283
|
+
this.stopAutoFlush();
|
284
|
+
this.stopProcessing();
|
285
|
+
this.clear();
|
286
|
+
}
|
287
|
+
|
288
|
+
/**
|
289
|
+
* Schedule processing of queued operations
|
290
|
+
*/
|
291
|
+
private scheduleProcessing(): void {
|
292
|
+
if (this.isProcessing || this.rafId) {
|
293
|
+
return;
|
294
|
+
}
|
295
|
+
|
296
|
+
this.rafId = requestAnimationFrame(() => {
|
297
|
+
this.processOperations();
|
298
|
+
this.rafId = null;
|
299
|
+
});
|
300
|
+
}
|
301
|
+
|
302
|
+
/**
|
303
|
+
* Process queued operations with read/write separation
|
304
|
+
*/
|
305
|
+
private async processOperations(forceFlush: boolean = false): Promise<void> {
|
306
|
+
if (this.isProcessing) {
|
307
|
+
return;
|
308
|
+
}
|
309
|
+
|
310
|
+
this.isProcessing = true;
|
311
|
+
const startTime = performance.now();
|
312
|
+
|
313
|
+
try {
|
314
|
+
if (this.config.separateReadWrite) {
|
315
|
+
// Process all reads first to avoid layout thrashing
|
316
|
+
await this.processQueue(this.readQueue, 'read', forceFlush);
|
317
|
+
|
318
|
+
// Then process all writes
|
319
|
+
await this.processQueue(this.writeQueue, 'write', forceFlush);
|
320
|
+
} else {
|
321
|
+
// Process mixed operations (less efficient but simpler)
|
322
|
+
const allOperations = [...this.readQueue, ...this.writeQueue];
|
323
|
+
this.readQueue = [];
|
324
|
+
this.writeQueue = [];
|
325
|
+
|
326
|
+
await this.processQueue(allOperations, 'mixed', forceFlush);
|
327
|
+
}
|
328
|
+
|
329
|
+
// Update metrics
|
330
|
+
const processingTime = performance.now() - startTime;
|
331
|
+
this.updateMetrics(processingTime);
|
332
|
+
|
333
|
+
} finally {
|
334
|
+
this.isProcessing = false;
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
/**
|
339
|
+
* Process a specific queue of operations
|
340
|
+
*/
|
341
|
+
private async processQueue(
|
342
|
+
queue: DOMOperation[],
|
343
|
+
queueType: 'read' | 'write' | 'mixed',
|
344
|
+
forceFlush: boolean
|
345
|
+
): Promise<void> {
|
346
|
+
if (queue.length === 0) {
|
347
|
+
return;
|
348
|
+
}
|
349
|
+
|
350
|
+
// Sort by priority and dependencies
|
351
|
+
const sortedOperations = this.sortOperations(queue);
|
352
|
+
const budget = forceFlush ? Infinity : this.config.frameTimeLimit;
|
353
|
+
const startTime = performance.now();
|
354
|
+
let processedCount = 0;
|
355
|
+
|
356
|
+
for (const operation of sortedOperations) {
|
357
|
+
// Check time budget
|
358
|
+
const elapsed = performance.now() - startTime;
|
359
|
+
if (!forceFlush && elapsed > budget) {
|
360
|
+
break;
|
361
|
+
}
|
362
|
+
|
363
|
+
// Check dependencies
|
364
|
+
if (!this.areDependenciesSatisfied(operation)) {
|
365
|
+
continue;
|
366
|
+
}
|
367
|
+
|
368
|
+
// Check batch size limit
|
369
|
+
if (!forceFlush && processedCount >= this.config.maxBatchSize) {
|
370
|
+
break;
|
371
|
+
}
|
372
|
+
|
373
|
+
try {
|
374
|
+
// Detect potential layout thrashing
|
375
|
+
if (this.config.measurePerformance && this.wouldCauseLayoutThrash(operation, queueType)) {
|
376
|
+
this.metrics.layoutThrashes++;
|
377
|
+
}
|
378
|
+
|
379
|
+
// Execute operation
|
380
|
+
operation.operation();
|
381
|
+
processedCount++;
|
382
|
+
|
383
|
+
// Update metrics
|
384
|
+
if (operation.type === 'read') {
|
385
|
+
this.metrics.readOperations++;
|
386
|
+
} else {
|
387
|
+
this.metrics.writeOperations++;
|
388
|
+
}
|
389
|
+
|
390
|
+
} catch (error) {
|
391
|
+
console.error('Error processing DOM operation:', error);
|
392
|
+
}
|
393
|
+
|
394
|
+
// Remove from queue
|
395
|
+
const index = queue.indexOf(operation);
|
396
|
+
if (index > -1) {
|
397
|
+
queue.splice(index, 1);
|
398
|
+
}
|
399
|
+
}
|
400
|
+
|
401
|
+
this.metrics.totalOperations += processedCount;
|
402
|
+
}
|
403
|
+
|
404
|
+
/**
|
405
|
+
* Sort operations by priority and dependencies
|
406
|
+
*/
|
407
|
+
private sortOperations(operations: DOMOperation[]): DOMOperation[] {
|
408
|
+
return operations.sort((a, b) => {
|
409
|
+
// Priority first
|
410
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
411
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
412
|
+
|
413
|
+
if (priorityDiff !== 0) {
|
414
|
+
return priorityDiff;
|
415
|
+
}
|
416
|
+
|
417
|
+
// Then by timestamp (older first)
|
418
|
+
return a.timestamp - b.timestamp;
|
419
|
+
});
|
420
|
+
}
|
421
|
+
|
422
|
+
/**
|
423
|
+
* Check if operation dependencies are satisfied
|
424
|
+
*/
|
425
|
+
private areDependenciesSatisfied(operation: DOMOperation): boolean {
|
426
|
+
if (!operation.dependencies || operation.dependencies.length === 0) {
|
427
|
+
return true;
|
428
|
+
}
|
429
|
+
|
430
|
+
return operation.dependencies.every(depId =>
|
431
|
+
this.operationResults.has(depId)
|
432
|
+
);
|
433
|
+
}
|
434
|
+
|
435
|
+
/**
|
436
|
+
* Detect potential layout thrashing
|
437
|
+
*/
|
438
|
+
private wouldCauseLayoutThrash(operation: DOMOperation, queueType: string): boolean {
|
439
|
+
// Simplified heuristic: write after read in same frame might cause thrashing
|
440
|
+
if (queueType === 'mixed' && operation.type === 'write') {
|
441
|
+
const recentReads = this.processingQueue.filter(op =>
|
442
|
+
op.type === 'read' &&
|
443
|
+
performance.now() - op.timestamp < 16
|
444
|
+
);
|
445
|
+
|
446
|
+
return recentReads.length > 0;
|
447
|
+
}
|
448
|
+
|
449
|
+
return false;
|
450
|
+
}
|
451
|
+
|
452
|
+
/**
|
453
|
+
* Generate unique operation ID
|
454
|
+
*/
|
455
|
+
private generateOperationId(type: string): string {
|
456
|
+
return `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
457
|
+
}
|
458
|
+
|
459
|
+
/**
|
460
|
+
* Start auto-flush timer
|
461
|
+
*/
|
462
|
+
private startAutoFlush(): void {
|
463
|
+
this.flushTimer = window.setInterval(() => {
|
464
|
+
if (this.readQueue.length > 0 || this.writeQueue.length > 0) {
|
465
|
+
this.scheduleProcessing();
|
466
|
+
}
|
467
|
+
}, this.config.flushInterval);
|
468
|
+
}
|
469
|
+
|
470
|
+
/**
|
471
|
+
* Stop auto-flush timer
|
472
|
+
*/
|
473
|
+
private stopAutoFlush(): void {
|
474
|
+
if (this.flushTimer) {
|
475
|
+
clearInterval(this.flushTimer);
|
476
|
+
this.flushTimer = null;
|
477
|
+
}
|
478
|
+
}
|
479
|
+
|
480
|
+
/**
|
481
|
+
* Stop processing
|
482
|
+
*/
|
483
|
+
private stopProcessing(): void {
|
484
|
+
if (this.rafId) {
|
485
|
+
cancelAnimationFrame(this.rafId);
|
486
|
+
this.rafId = null;
|
487
|
+
}
|
488
|
+
}
|
489
|
+
|
490
|
+
/**
|
491
|
+
* Update performance metrics
|
492
|
+
*/
|
493
|
+
private updateMetrics(processingTime: number): void {
|
494
|
+
this.metrics.batchesProcessed++;
|
495
|
+
this.metrics.averageBatchTime =
|
496
|
+
(this.metrics.averageBatchTime + processingTime) / 2;
|
497
|
+
|
498
|
+
// Calculate prevented thrashes (simplified)
|
499
|
+
if (this.config.separateReadWrite) {
|
500
|
+
const potentialThrashes = Math.min(this.readQueue.length, this.writeQueue.length);
|
501
|
+
this.metrics.preventedThrashes += potentialThrashes;
|
502
|
+
}
|
503
|
+
}
|
504
|
+
|
505
|
+
/**
|
506
|
+
* Create initial metrics
|
507
|
+
*/
|
508
|
+
private createInitialMetrics(): BatchMetrics {
|
509
|
+
return {
|
510
|
+
totalOperations: 0,
|
511
|
+
readOperations: 0,
|
512
|
+
writeOperations: 0,
|
513
|
+
batchesProcessed: 0,
|
514
|
+
averageBatchTime: 0,
|
515
|
+
layoutThrashes: 0,
|
516
|
+
preventedThrashes: 0
|
517
|
+
};
|
518
|
+
}
|
519
|
+
}
|