onelaraveljs 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.
Files changed (67) hide show
  1. package/README.md +87 -0
  2. package/docs/integration_analysis.md +116 -0
  3. package/docs/onejs_analysis.md +108 -0
  4. package/docs/optimization_implementation_group2.md +458 -0
  5. package/docs/optimization_plan.md +130 -0
  6. package/index.js +16 -0
  7. package/package.json +13 -0
  8. package/src/app.js +61 -0
  9. package/src/core/API.js +72 -0
  10. package/src/core/ChildrenRegistry.js +410 -0
  11. package/src/core/DOMBatcher.js +207 -0
  12. package/src/core/ErrorBoundary.js +226 -0
  13. package/src/core/EventDelegator.js +416 -0
  14. package/src/core/Helper.js +817 -0
  15. package/src/core/LoopContext.js +97 -0
  16. package/src/core/OneDOM.js +246 -0
  17. package/src/core/OneMarkup.js +444 -0
  18. package/src/core/Router.js +996 -0
  19. package/src/core/SEOConfig.js +321 -0
  20. package/src/core/SectionEngine.js +75 -0
  21. package/src/core/TemplateEngine.js +83 -0
  22. package/src/core/View.js +273 -0
  23. package/src/core/ViewConfig.js +229 -0
  24. package/src/core/ViewController.js +1410 -0
  25. package/src/core/ViewControllerOptimized.js +164 -0
  26. package/src/core/ViewIdentifier.js +361 -0
  27. package/src/core/ViewLoader.js +272 -0
  28. package/src/core/ViewManager.js +1962 -0
  29. package/src/core/ViewState.js +761 -0
  30. package/src/core/ViewSystem.js +301 -0
  31. package/src/core/ViewTemplate.js +4 -0
  32. package/src/core/helpers/BindingHelper.js +239 -0
  33. package/src/core/helpers/ConfigHelper.js +37 -0
  34. package/src/core/helpers/EventHelper.js +172 -0
  35. package/src/core/helpers/LifecycleHelper.js +17 -0
  36. package/src/core/helpers/ReactiveHelper.js +169 -0
  37. package/src/core/helpers/RenderHelper.js +15 -0
  38. package/src/core/helpers/ResourceHelper.js +89 -0
  39. package/src/core/helpers/TemplateHelper.js +11 -0
  40. package/src/core/managers/BindingManager.js +671 -0
  41. package/src/core/managers/ConfigurationManager.js +136 -0
  42. package/src/core/managers/EventManager.js +309 -0
  43. package/src/core/managers/LifecycleManager.js +356 -0
  44. package/src/core/managers/ReactiveManager.js +334 -0
  45. package/src/core/managers/RenderEngine.js +292 -0
  46. package/src/core/managers/ResourceManager.js +441 -0
  47. package/src/core/managers/ViewHierarchyManager.js +258 -0
  48. package/src/core/managers/ViewTemplateManager.js +127 -0
  49. package/src/core/reactive/ReactiveComponent.js +592 -0
  50. package/src/core/services/EventService.js +418 -0
  51. package/src/core/services/HttpService.js +106 -0
  52. package/src/core/services/LoggerService.js +57 -0
  53. package/src/core/services/StateService.js +512 -0
  54. package/src/core/services/StorageService.js +856 -0
  55. package/src/core/services/StoreService.js +258 -0
  56. package/src/core/services/TemplateDetectorService.js +361 -0
  57. package/src/core/services/Test.js +18 -0
  58. package/src/helpers/devWarnings.js +205 -0
  59. package/src/helpers/performance.js +226 -0
  60. package/src/helpers/utils.js +287 -0
  61. package/src/init.js +343 -0
  62. package/src/plugins/auto-plugin.js +34 -0
  63. package/src/services/Test.js +18 -0
  64. package/src/types/index.js +193 -0
  65. package/src/utils/date-helper.js +51 -0
  66. package/src/utils/helpers.js +39 -0
  67. package/src/utils/validation.js +32 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * API Module
3
+ * ES6 Module for Blade Compiler
4
+ */
5
+
6
+ // Import App from global init
7
+ import { HttpService } from './services/HttpService.js';
8
+
9
+ export class AppAPI {
10
+ constructor() {
11
+ this.http = new HttpService();
12
+ this.http.setHeader('Content-Type', 'application/json');
13
+ this.http.setHeader('Accept', 'application/json');
14
+ this.http.setHeader('X-Requested-With', 'XMLHttpRequest');
15
+ // Conditional document access for Node.js compatibility
16
+ if (typeof document !== 'undefined') {
17
+ const csrfToken = document.querySelector('meta[name="csrf-token"]');
18
+ if (csrfToken) {
19
+ this.http.setHeader('X-CSRF-TOKEN', csrfToken.getAttribute('content'));
20
+ }
21
+ }
22
+ this.http.setHeader('X-DATA-TYPE', 'json');
23
+ this.endpoints = typeof window !== 'undefined' ? window.APP_CONFIGS?.api?.endpoints || {} : {};
24
+ }
25
+ async getViewData(uri) {
26
+ try {
27
+ const response = await this.http.get(uri).then(response => {
28
+ return response.status ? response.data : {};
29
+ }).catch(error => {
30
+ console.error('❌ AppAPI: Error getting view data:', error);
31
+ return {};
32
+ });
33
+
34
+ return response;
35
+ } catch (error) {
36
+ console.error('❌ AppAPI: Error getting view data:', error);
37
+ return {};
38
+ }
39
+
40
+ }
41
+ async getSystemConfig() {
42
+ return {};
43
+ return this.http.get('/api/system/config').then(response => {
44
+ return response.status ? response.data : {};
45
+ }).catch(error => {
46
+ console.error('❌ AppAPI: Error getting system config:', error);
47
+ return {};
48
+ });
49
+ }
50
+ async getSystemData() {
51
+ let endpoint = this.endpoints?.system?.data;
52
+ if (!endpoint) {
53
+ return {};
54
+ }
55
+ return this.http.get(endpoint).then(response => {
56
+ return response.status ? response.data : {};
57
+ }).catch(error => {
58
+ console.error('❌ AppAPI: Error getting system data:', error);
59
+ return {};
60
+ });
61
+ }
62
+
63
+ async getURIDAta() {
64
+ const uri = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
65
+ return this.getViewData(uri);
66
+ }
67
+
68
+ }
69
+
70
+ export const API = new AppAPI();
71
+
72
+ export default API;
@@ -0,0 +1,410 @@
1
+ /**
2
+ * ChildrenRegistry - Centralized Children Management System
3
+ *
4
+ * Replaces the complex 4-array tracking system (scanChildrenIDs, renewChildrenIDs, rcChildrenIDs, children)
5
+ * with a unified Map-based registry for better:
6
+ * - Memory management (automatic cleanup)
7
+ * - Lifecycle coordination (smart mount/unmount)
8
+ * - State synchronization (reactive data binding)
9
+ * - Performance (efficient queries and diffs)
10
+ *
11
+ * @see docs/dev/CHILDREN_VIEW_MANAGEMENT_ANALYSIS.md
12
+ * @see docs/dev/CONTROLLER_VIEW_SEPARATION.md
13
+ */
14
+
15
+ /**
16
+ * ChildrenRegistry Class
17
+ *
18
+ * Centralized management for all child views with:
19
+ * - Map-based storage for O(1) lookups
20
+ * - Automatic lifecycle management
21
+ * - Reactive data binding support
22
+ * - Memory leak prevention
23
+ */
24
+ export class ChildrenRegistry {
25
+ /**
26
+ * @param {ViewController} controller - Parent controller
27
+ */
28
+ constructor(controller) {
29
+ /** @type {ViewController} */
30
+ this.controller = controller;
31
+
32
+ /** @type {Map<string, Object>} Main registry: childId → ChildNode */
33
+ this.registry = new Map();
34
+
35
+ /** @type {Set<string>} Set of currently mounted child IDs */
36
+ this.mountedChildren = new Set();
37
+
38
+ /** @type {Map<string, Set<string>>} Reactive component → child IDs mapping */
39
+ this.reactiveComponentChildren = new Map();
40
+ }
41
+
42
+ /**
43
+ * Register a child view
44
+ *
45
+ * Stores the controller internally and provides view access via getter.
46
+ * Links child to parent reactive component if applicable.
47
+ *
48
+ * @param {View} child - Child view instance (child.__ is ViewController)
49
+ * @param {Object} options - Registration options
50
+ * @param {Object} options.data - Initial data for child
51
+ * @param {Object} options.parentReactiveComponent - Parent reactive component if applicable
52
+ * @param {number} options.index - Child index
53
+ * @returns {Object} ChildNode
54
+ */
55
+ register(child, options = {}) {
56
+ const {
57
+ data = {},
58
+ parentReactiveComponent = null,
59
+ index = this.registry.size
60
+ } = options;
61
+
62
+ const childCtrl = child.__; // Get controller from view
63
+
64
+ const childNode = {
65
+ controller: childCtrl,
66
+ get view() {
67
+ return childCtrl.view; // ✅ Fixed: return child controller's view
68
+ },
69
+ state: 'pending',
70
+ lifecycle: {
71
+ created: Date.now(),
72
+ mounted: null,
73
+ unmounted: null
74
+ },
75
+ scope: {
76
+ name: childCtrl.view.path,
77
+ id: childCtrl.view.id,
78
+ index,
79
+ data: this._createReactiveData(data),
80
+ subscriptions: new Set()
81
+ },
82
+ parent: this.controller,
83
+ parentReactiveComponent
84
+ };
85
+
86
+ this.registry.set(childCtrl.view.id, childNode);
87
+
88
+ // Track in reactive component if applicable
89
+ if (parentReactiveComponent) {
90
+ if (!this.reactiveComponentChildren.has(parentReactiveComponent.id)) {
91
+ this.reactiveComponentChildren.set(parentReactiveComponent.id, new Set());
92
+ }
93
+ this.reactiveComponentChildren.get(parentReactiveComponent.id).add(childCtrl.view.id);
94
+ }
95
+
96
+ // Link to main children array (backward compatibility)
97
+ if (!this.controller.children.includes(childCtrl)) {
98
+ this.controller.children.push(childCtrl);
99
+ }
100
+
101
+ return childNode;
102
+ }
103
+
104
+ /**
105
+ * Create reactive data proxy for child scope
106
+ *
107
+ * Wraps data in a Proxy to detect changes and notify children.
108
+ *
109
+ * @param {Object} data - Initial data object
110
+ * @returns {Object} Proxied data object
111
+ */
112
+ _createReactiveData(data) {
113
+ return new Proxy(data, {
114
+ set: (target, key, value) => {
115
+ const oldValue = target[key];
116
+ target[key] = value;
117
+
118
+ // Notify child about data change
119
+ if (oldValue !== value) {
120
+ this._notifyChildDataChange(key, value);
121
+ }
122
+
123
+ return true;
124
+ }
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Get child node by ID
130
+ *
131
+ * @param {string} childId - Child view ID
132
+ * @returns {Object|undefined} ChildNode or undefined
133
+ */
134
+ get(childId) {
135
+ return this.registry.get(childId);
136
+ }
137
+
138
+ /**
139
+ * Get all children for a reactive component
140
+ *
141
+ * @param {string} reactiveComponentId - Reactive component ID
142
+ * @returns {Array<Object>} Array of ChildNodes
143
+ */
144
+ getReactiveComponentChildren(reactiveComponentId) {
145
+ const childIds = this.reactiveComponentChildren.get(reactiveComponentId);
146
+ if (!childIds) return [];
147
+
148
+ return Array.from(childIds)
149
+ .map(id => this.registry.get(id))
150
+ .filter(node => node !== undefined);
151
+ }
152
+
153
+ /**
154
+ * Mark child as mounted
155
+ *
156
+ * Updates state, timestamps, and calls child's mounted lifecycle hook.
157
+ *
158
+ * @param {string} childId - Child view ID
159
+ */
160
+ mount(childId) {
161
+ const childNode = this.registry.get(childId);
162
+ if (!childNode) return;
163
+
164
+ // Prevent double mounting
165
+ if (childNode.state === 'mounted' || this.mountedChildren.has(childId)) {
166
+ return;
167
+ }
168
+
169
+ childNode.state = 'mounted';
170
+ childNode.lifecycle.mounted = Date.now();
171
+ this.mountedChildren.add(childId);
172
+
173
+ // Call child controller's mounted hook
174
+ if (childNode.controller._lifecycleManager) {
175
+ childNode.controller._lifecycleManager.mounted();
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Unmount child
181
+ *
182
+ * Updates state, timestamps, and calls child's unmounted lifecycle hook.
183
+ *
184
+ * @param {string} childId - Child view ID
185
+ */
186
+ unmount(childId) {
187
+ const childNode = this.registry.get(childId);
188
+ if (!childNode) return;
189
+
190
+ // Only unmount if currently mounted
191
+ if (childNode.state !== 'mounted' && !this.mountedChildren.has(childId)) {
192
+ return;
193
+ }
194
+
195
+ childNode.state = 'unmounted';
196
+ childNode.lifecycle.unmounted = Date.now();
197
+ this.mountedChildren.delete(childId);
198
+
199
+ // Call child controller's unmounted hook
200
+ if (childNode.controller._lifecycleManager) {
201
+ childNode.controller._lifecycleManager.unmounted();
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Unregister and destroy child
207
+ *
208
+ * Performs complete cleanup:
209
+ * 1. Unmounts if still mounted
210
+ * 2. Clears subscriptions
211
+ * 3. Removes from reactive component tracking
212
+ * 4. Destroys child controller
213
+ * 5. Removes from registry and main children array
214
+ *
215
+ * @param {string} childId - Child view ID
216
+ */
217
+ destroy(childId) {
218
+ const childNode = this.registry.get(childId);
219
+ if (!childNode) return;
220
+
221
+ // Unmount if still mounted
222
+ if (this.mountedChildren.has(childId)) {
223
+ this.unmount(childId);
224
+ }
225
+
226
+ // Clean up subscriptions
227
+ childNode.scope.subscriptions.forEach(unsub => unsub());
228
+ childNode.scope.subscriptions.clear();
229
+
230
+ // Remove from reactive component tracking
231
+ if (childNode.parentReactiveComponent) {
232
+ const rcId = childNode.parentReactiveComponent.id;
233
+ this.reactiveComponentChildren.get(rcId)?.delete(childId);
234
+ }
235
+
236
+ // Destroy child controller
237
+ if (childNode.controller._lifecycleManager) {
238
+ childNode.controller._lifecycleManager.destroy();
239
+ }
240
+
241
+ // Remove from registry
242
+ this.registry.delete(childId);
243
+
244
+ // Note: Removal from controller.children array is handled by ViewHierarchyManager.removeChild()
245
+ // This keeps separation of concerns - ViewHierarchyManager manages the children array,
246
+ // ChildrenRegistry only manages the registry Map and tracking Sets
247
+ }
248
+
249
+ /**
250
+ * Find children that can be reused (by path)
251
+ *
252
+ * Searches for unmounted children with matching path for reuse during
253
+ * reactive component updates.
254
+ *
255
+ * @param {string} path - View path to search for
256
+ * @param {boolean} excludeMounted - Exclude mounted children from results
257
+ * @returns {Array<{id: string, childNode: Object}>} Array of candidates
258
+ */
259
+ findReusable(path, excludeMounted = true) {
260
+ const candidates = [];
261
+
262
+ for (const [id, childNode] of this.registry) {
263
+ if (childNode.controller.view.path === path) {
264
+ if (excludeMounted && this.mountedChildren.has(id)) {
265
+ continue;
266
+ }
267
+ candidates.push({ id, childNode });
268
+ }
269
+ }
270
+
271
+ return candidates;
272
+ }
273
+
274
+ /**
275
+ * Clean up orphaned children
276
+ *
277
+ * Destroys children that are not in the active IDs set and are unmounted.
278
+ * Useful for cleaning up after reactive component updates.
279
+ *
280
+ * @param {Array<string>} activeIds - Set of currently active child IDs
281
+ * @returns {number} Number of children destroyed
282
+ */
283
+ cleanupOrphaned(activeIds) {
284
+ const activeSet = new Set(activeIds);
285
+ const toDestroy = [];
286
+
287
+ for (const [id, childNode] of this.registry) {
288
+ if (!activeSet.has(id) && childNode.state !== 'mounted') {
289
+ toDestroy.push(id);
290
+ }
291
+ }
292
+
293
+ toDestroy.forEach(id => this.destroy(id));
294
+
295
+ return toDestroy.length;
296
+ }
297
+
298
+ /**
299
+ * Get all child IDs (for backward compatibility)
300
+ *
301
+ * @returns {Array<string>} Array of child IDs
302
+ */
303
+ getAllIds() {
304
+ return Array.from(this.registry.keys());
305
+ }
306
+
307
+ /**
308
+ * Update parent reactive component for a child
309
+ * Used when a child is reused in a different reactive component
310
+ *
311
+ * @param {string} childId - Child view ID
312
+ * @param {Object} newParentRC - New parent reactive component
313
+ */
314
+ updateParentReactiveComponent(childId, newParentRC) {
315
+ const childNode = this.registry.get(childId);
316
+ if (!childNode) return;
317
+
318
+ // Remove from old reactive component tracking
319
+ if (childNode.parentReactiveComponent) {
320
+ const oldRcId = childNode.parentReactiveComponent.id;
321
+ this.reactiveComponentChildren.get(oldRcId)?.delete(childId);
322
+
323
+ // Clean up empty set
324
+ if (this.reactiveComponentChildren.get(oldRcId)?.size === 0) {
325
+ this.reactiveComponentChildren.delete(oldRcId);
326
+ }
327
+ }
328
+
329
+ // Update to new reactive component
330
+ childNode.parentReactiveComponent = newParentRC;
331
+
332
+ // Add to new reactive component tracking
333
+ if (newParentRC) {
334
+ if (!this.reactiveComponentChildren.has(newParentRC.id)) {
335
+ this.reactiveComponentChildren.set(newParentRC.id, new Set());
336
+ }
337
+ this.reactiveComponentChildren.get(newParentRC.id).add(childId);
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Get all mounted child IDs
343
+ *
344
+ * @returns {Array<string>} Array of mounted child IDs
345
+ */
346
+ getMountedIds() {
347
+ return Array.from(this.mountedChildren);
348
+ }
349
+
350
+ /**
351
+ * Check if child is mounted
352
+ *
353
+ * @param {string} childId - Child view ID
354
+ * @returns {boolean} true if mounted
355
+ */
356
+ isMounted(childId) {
357
+ return this.mountedChildren.has(childId);
358
+ }
359
+
360
+ /**
361
+ * Get registry size
362
+ *
363
+ * @returns {number} Number of registered children
364
+ */
365
+ get size() {
366
+ return this.registry.size;
367
+ }
368
+
369
+ /**
370
+ * Clear all children
371
+ *
372
+ * Destroys all children and clears all tracking structures.
373
+ */
374
+ /**
375
+ * Clear all children
376
+ *
377
+ * Destroys all registered children and clears all tracking.
378
+ */
379
+ clear() {
380
+ const allChildIds = Array.from(this.registry.keys());
381
+ allChildIds.forEach(childId => {
382
+ this.destroy(childId);
383
+ });
384
+
385
+ this.registry.clear();
386
+ this.mountedChildren.clear();
387
+ this.reactiveComponentChildren.clear();
388
+
389
+ // Clear children array (ViewHierarchyManager's responsibility, but clear() is special case)
390
+ if (this.controller.children) {
391
+ this.controller.children = [];
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Notify child about parent data change
397
+ *
398
+ * Called when reactive data in child scope changes.
399
+ * Children can subscribe to these changes for automatic updates.
400
+ *
401
+ * @param {string} key - Data key that changed
402
+ * @param {any} value - New value
403
+ * @private
404
+ */
405
+ _notifyChildDataChange(key, value) {
406
+ // Implementation depends on child's subscription mechanism
407
+ // This is a placeholder for future reactive binding features
408
+ // Currently, subscriptions are managed manually via scope.subscriptions
409
+ }
410
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * DOMBatcher
3
+ *
4
+ * Tối ưu hóa các thao tác DOM bằng cách gom nhóm đọc và ghi để ngăn layout thrashing.
5
+ * Sử dụng RequestAnimationFrame để lên lịch các thao tác theo batch.
6
+ *
7
+ * Lợi ích hiệu suất:
8
+ * - Ngăn chặn forced synchronous layouts (reflows)
9
+ * - Gom nhóm đọc trước ghi trong mỗi frame
10
+ * - Giảm tính toán layout từ O(n²) xuống O(n)
11
+ * - Cải thiện hiệu suất render cho UI động
12
+ *
13
+ * Mẫu sử dụng:
14
+ * ```javascript
15
+ * // XẤU: Gây layout thrashing
16
+ * element1.style.width = element2.offsetWidth + 'px'; // đọc-ghi
17
+ * element3.style.height = element4.offsetHeight + 'px'; // đọc-ghi
18
+ *
19
+ * // TỐT: Thao tác theo batch
20
+ * DOMBatcher.read(() => element2.offsetWidth)
21
+ * .then(width => DOMBatcher.write(() => element1.style.width = width + 'px'));
22
+ * DOMBatcher.read(() => element4.offsetHeight)
23
+ * .then(height => DOMBatcher.write(() => element3.style.height = height + 'px'));
24
+ * ```
25
+ *
26
+ * @class DOMBatcher
27
+ * @version 1.0.0
28
+ * @since 2025-01-06
29
+ */
30
+ class DOMBatcher {
31
+ constructor() {
32
+ /** @type {Array<{fn: Function, resolve: Function, reject: Function}>} */
33
+ this.readQueue = [];
34
+
35
+ /** @type {Array<{fn: Function, resolve: Function, reject: Function}>} */
36
+ this.writeQueue = [];
37
+
38
+ /** @type {number|null} */
39
+ this.rafId = null;
40
+
41
+ /** @type {boolean} */
42
+ this.isProcessing = false;
43
+ }
44
+
45
+ /**
46
+ * Lên lịch thao tác đọc DOM
47
+ * Tất cả thao tác đọc được gom nhóm và thực thi trước ghi trong frame tiếp theo
48
+ *
49
+ * @param {Function} fn - Hàm đọc từ DOM (trả về giá trị)
50
+ * @returns {Promise<any>} Promise resolve với giá trị đọc được
51
+ *
52
+ * @example
53
+ * const width = await DOMBatcher.read(() => element.offsetWidth);
54
+ * const rect = await DOMBatcher.read(() => element.getBoundingClientRect());
55
+ */
56
+ read(fn) {
57
+ return new Promise((resolve, reject) => {
58
+ this.readQueue.push({ fn, resolve, reject });
59
+ this.scheduleFlush();
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Lên lịch thao tác ghi DOM
65
+ * Tất cả thao tác ghi được gom nhóm và thực thi sau đọc trong frame tiếp theo
66
+ *
67
+ * @param {Function} fn - Hàm ghi vào DOM
68
+ * @returns {Promise<void>} Promise resolve khi ghi hoàn thành
69
+ *
70
+ * @example
71
+ * await DOMBatcher.write(() => element.style.width = '100px');
72
+ * await DOMBatcher.write(() => element.classList.add('active'));
73
+ */
74
+ write(fn) {
75
+ return new Promise((resolve, reject) => {
76
+ this.writeQueue.push({ fn, resolve, reject });
77
+ this.scheduleFlush();
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Lên lịch flush nếu chưa được lên lịch
83
+ * Sử dụng RAF để gom nhóm thao tác trong frame tiếp theo
84
+ *
85
+ * @private
86
+ */
87
+ scheduleFlush() {
88
+ if (this.rafId !== null) {
89
+ return; // Đã được lên lịch
90
+ }
91
+
92
+ this.rafId = requestAnimationFrame(() => {
93
+ this.flush();
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Thực thi tất cả thao tác đã gom nhóm
99
+ * Đọc trước, sau đó ghi để ngăn layout thrashing
100
+ *
101
+ * @private
102
+ */
103
+ flush() {
104
+ if (this.isProcessing) {
105
+ // Lên lịch lại nếu đang xử lý
106
+ this.rafId = null;
107
+ this.scheduleFlush();
108
+ return;
109
+ }
110
+
111
+ this.isProcessing = true;
112
+ this.rafId = null;
113
+
114
+ try {
115
+ // Giai đoạn 1: Thực thi tất cả thao tác đọc
116
+ const reads = this.readQueue.splice(0);
117
+ reads.forEach(({ fn, resolve, reject }) => {
118
+ try {
119
+ const result = fn();
120
+ resolve(result);
121
+ } catch (error) {
122
+ console.error('[DOMBatcher] Read error:', error);
123
+ reject(error);
124
+ }
125
+ });
126
+
127
+ // Giai đoạn 2: Thực thi tất cả thao tác ghi
128
+ const writes = this.writeQueue.splice(0);
129
+ writes.forEach(({ fn, resolve, reject }) => {
130
+ try {
131
+ fn();
132
+ resolve();
133
+ } catch (error) {
134
+ console.error('[DOMBatcher] Write error:', error);
135
+ reject(error);
136
+ }
137
+ });
138
+ } finally {
139
+ this.isProcessing = false;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Xóa tất cả thao tác đang chờ và hủy RAF
145
+ * Dùng để dọn dẹp hoặc khôi phục lỗi
146
+ */
147
+ clear() {
148
+ if (this.rafId !== null) {
149
+ cancelAnimationFrame(this.rafId);
150
+ this.rafId = null;
151
+ }
152
+
153
+ this.readQueue.length = 0;
154
+ this.writeQueue.length = 0;
155
+ this.isProcessing = false;
156
+ }
157
+
158
+ /**
159
+ * Đo nhiều element cùng lúc (đọc theo batch tối ưu)
160
+ *
161
+ * @param {Array<{element: HTMLElement, properties: string[]}>} measurements
162
+ * @returns {Promise<Array<Object>>} Mảng kết quả đo
163
+ *
164
+ * @example
165
+ * const results = await DOMBatcher.measure([
166
+ * { element: el1, properties: ['offsetWidth', 'offsetHeight'] },
167
+ * { element: el2, properties: ['clientWidth', 'scrollTop'] }
168
+ * ]);
169
+ */
170
+ measure(measurements) {
171
+ return this.read(() => {
172
+ return measurements.map(({ element, properties }) => {
173
+ const result = {};
174
+ properties.forEach(prop => {
175
+ result[prop] = element[prop];
176
+ });
177
+ return result;
178
+ });
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Áp dụng nhiều thay đổi style cùng lúc (ghi theo batch tối ưu)
184
+ *
185
+ * @param {Array<{element: HTMLElement, styles: Object}>} styleChanges
186
+ * @returns {Promise<void>}
187
+ *
188
+ * @example
189
+ * await DOMBatcher.applyStyles([
190
+ * { element: el1, styles: { width: '100px', height: '50px' } },
191
+ * { element: el2, styles: { display: 'none' } }
192
+ * ]);
193
+ */
194
+ applyStyles(styleChanges) {
195
+ return this.write(() => {
196
+ styleChanges.forEach(({ element, styles }) => {
197
+ Object.entries(styles).forEach(([prop, value]) => {
198
+ element.style[prop] = value;
199
+ });
200
+ });
201
+ });
202
+ }
203
+ }
204
+
205
+ // Export instance singleton
206
+ const batcher = new DOMBatcher();
207
+ export default batcher;