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,671 @@
1
+ import OneDOM from '../OneDOM.js';
2
+ import logger from '../services/LoggerService.js';
3
+ import DOMBatcher from '../DOMBatcher.js';
4
+
5
+ /**
6
+ * BindingManager
7
+ * Quản lý tất cả các loại binding trong view:
8
+ * - Input bindings (two-way data binding)
9
+ * - Attribute bindings (reactive HTML attributes)
10
+ * - Class bindings (reactive CSS classes)
11
+ * - Thông báo thay đổi state
12
+ *
13
+ * Quản lý bộ nhớ:
14
+ * - Sử dụng WeakMap cho element references (tự động garbage collection)
15
+ * - Dọn dẹp đúng cách trong method destroy()
16
+ * - Validate element.isConnected trước khi thao tác
17
+ *
18
+ * @class BindingManager
19
+ * @since 2.0.0 - Sửa memory leak và cải thiện hiệu suất
20
+ */
21
+ export class BindingManager {
22
+ constructor(controller) {
23
+ this.controller = controller;
24
+
25
+ // Input binding (two-way data binding)
26
+ this.bindingEventListeners = [];
27
+ this.isBindingEventListenerStarted = false;
28
+
29
+ /**
30
+ * WeakMap lưu các cờ cập nhật riêng cho từng element
31
+ * Ngăn cập nhật vòng tròn trong two-way binding
32
+ * Tự động garbage collected khi element bị xóa
33
+ * @type {WeakMap<HTMLElement, {pushing: boolean, syncing: boolean}>}
34
+ */
35
+ this.elementUpdateFlags = new WeakMap();
36
+
37
+ /**
38
+ * WeakMap lưu mapping element-to-listener
39
+ * Để dọn dẹp và tra cứu hiệu quả
40
+ * @type {WeakMap<HTMLElement, {eventType: string, handler: Function, stateKey: string}>}
41
+ */
42
+ this.elementListenerMap = new WeakMap();
43
+
44
+ // Attribute binding (reactive attributes)
45
+ this.attributeConfigs = [];
46
+ this.attributeListeners = [];
47
+ this.attributeIndex = 0;
48
+
49
+ // Class binding (reactive CSS classes)
50
+ this.classBindingConfigs = [];
51
+ this.classBindingListeners = [];
52
+ this.isClassBindingReadyToListen = false;
53
+ }
54
+
55
+ /**
56
+ * Reset trạng thái binding manager
57
+ * Xóa listeners nhưng KHÔNG xóa event listeners khỏi DOM
58
+ * Dùng destroy() để dọn dẹp hoàn toàn
59
+ */
60
+ reset() {
61
+ this.bindingEventListeners = [];
62
+ this.attributeListeners = [];
63
+ }
64
+
65
+ /**
66
+ * Bắt đầu two-way data binding event listeners
67
+ *
68
+ * Tính năng:
69
+ * - Chỉ xử lý các DOM element đã kết nối
70
+ * - Sử dụng WeakMap để quản lý bộ nhớ tự động
71
+ * - Ngăn listener trùng lặp trên cùng element
72
+ * - Đồng bộ state ban đầu vào element
73
+ * - Có thể gọi nhiều lần an toàn (idempotent)
74
+ *
75
+ * @returns {void}
76
+ *
77
+ * @example
78
+ * // Trong mounted lifecycle
79
+ * this._bindingManager.startBindingEventListener();
80
+ */
81
+ startBindingEventListener() {
82
+ const selector = `[data-binding][data-view-id="${this.controller.id}"]`;
83
+ const inputs = document.querySelectorAll(selector);
84
+
85
+ if (inputs.length === 0) {
86
+ logger.log(`[BindingManager] No binding inputs found for view: ${this.controller.id}`);
87
+ return;
88
+ }
89
+
90
+ let attachedCount = 0;
91
+ let skippedCount = 0;
92
+
93
+ inputs.forEach(input => {
94
+ // Validate element vẫn kết nối với DOM
95
+ if (!input.isConnected) {
96
+ logger.warn('[BindingManager] Skipping disconnected element', input);
97
+ skippedCount++;
98
+ return;
99
+ }
100
+
101
+ // Kiểm tra listener đã tồn tại cho element này chưa
102
+ if (this.elementListenerMap.has(input)) {
103
+ // Đã bind, bỏ qua
104
+ skippedCount++;
105
+ return;
106
+ }
107
+
108
+ const stateKey = input.getAttribute('data-binding');
109
+ if (!stateKey) {
110
+ logger.warn('[BindingManager] Missing data-binding attribute', input);
111
+ skippedCount++;
112
+ return;
113
+ }
114
+
115
+ // Trích xuất root key cho state change subscription
116
+ // ví dụ: 'formData.name' -> 'formData'
117
+ const rootStateKey = stateKey.split('.')[0];
118
+ const tag = input.tagName.toLowerCase();
119
+ const eventType = tag === 'select' ? 'change' : 'input';
120
+
121
+ // Đồng bộ ban đầu: state → element
122
+ this.syncStateToElement(input, stateKey);
123
+
124
+ // Tạo event handler
125
+ const handler = (event) => this.pushElementToState(input, stateKey);
126
+
127
+ // Gắn listener
128
+ input.addEventListener(eventType, handler);
129
+
130
+ // Lưu trong WeakMap để tra cứu hiệu quả và tự động dọn dẹp
131
+ this.elementListenerMap.set(input, {
132
+ eventType,
133
+ handler,
134
+ stateKey
135
+ });
136
+
137
+ // Lưu trong array để tương thích (sẽ được dọn dẹp trong destroy)
138
+ // Lưu ý: Lưu đầy đủ stateKey (ví dụ: 'formData.name') không chỉ rootKey
139
+ this.bindingEventListeners.push({
140
+ element: input,
141
+ key: rootStateKey, // Cho subscription cấp root
142
+ stateKey: stateKey, // Đường dẫn đầy đủ cho nested sync
143
+ eventType,
144
+ handler
145
+ });
146
+
147
+ attachedCount++;
148
+ });
149
+
150
+ this.isBindingEventListenerStarted = true;
151
+
152
+ if (attachedCount > 0) {
153
+ logger.log(`[BindingManager] Attached ${attachedCount} new binding listeners for view: ${this.controller.id}`);
154
+ }
155
+ if (skippedCount > 0) {
156
+ logger.log(`[BindingManager] Skipped ${skippedCount} already-bound elements for view: ${this.controller.id}`);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Dừng two-way data binding event listeners
162
+ * Xóa tất cả event listeners và xóa cấu trúc theo dõi
163
+ *
164
+ * @returns {void}
165
+ *
166
+ * @example
167
+ * // Trong beforeDestroy lifecycle
168
+ * this._bindingManager.stopBindingEventListener();
169
+ */
170
+ stopBindingEventListener() {
171
+ if (!this.isBindingEventListenerStarted) {
172
+ return;
173
+ }
174
+
175
+ let removedCount = 0;
176
+ this.bindingEventListeners.forEach(({ element, eventType, handler }) => {
177
+ try {
178
+ // Xóa listener an toàn ngay cả khi element đã ngắt kết nối
179
+ element.removeEventListener(eventType, handler);
180
+
181
+ // Xóa khỏi WeakMap tracking
182
+ if (this.elementListenerMap.has(element)) {
183
+ this.elementListenerMap.delete(element);
184
+ }
185
+
186
+ removedCount++;
187
+ } catch (error) {
188
+ logger.error('[BindingManager] Error removing event listener', error);
189
+ }
190
+ });
191
+
192
+ // Xóa mảng
193
+ this.bindingEventListeners = [];
194
+
195
+ // Lưu ý: Các entry trong WeakMap sẽ được garbage collected tự động
196
+ // khi elements bị xóa khỏi DOM
197
+
198
+ this.isBindingEventListenerStarted = false;
199
+ logger.log(`[BindingManager] Removed ${removedCount} binding listeners`);
200
+ }
201
+
202
+ /**
203
+ * Đẩy giá trị element vào state (element → state)
204
+ * Sử dụng trong two-way binding khi giá trị element thay đổi
205
+ *
206
+ * Tính năng:
207
+ * - Ngăn cập nhật vòng tròn dùng cờ WeakMap
208
+ * - Validate element đang kết nối
209
+ * - Xóa cờ async để hiệu suất tốt hơn
210
+ *
211
+ * @param {HTMLElement} element - Input element
212
+ * @param {string} stateKey - State key (hỗ trợ nested keys như 'user.name')
213
+ * @returns {void}
214
+ *
215
+ * @example
216
+ * // Tự động gọi bởi event listener
217
+ * input.addEventListener('input', (e) => {
218
+ * this.pushElementToState(e.target, 'username');
219
+ * });
220
+ */
221
+ pushElementToState(element, stateKey) {
222
+ // Validate element đang kết nối
223
+ if (!element || !element.isConnected) {
224
+ logger.warn('[BindingManager] Cannot push from disconnected element');
225
+ return;
226
+ }
227
+
228
+ // Lấy hoặc khởi tạo cờ cho element này
229
+ let flags = this.elementUpdateFlags.get(element);
230
+ if (!flags) {
231
+ flags = { pushing: false, syncing: false };
232
+ this.elementUpdateFlags.set(element, flags);
233
+ }
234
+
235
+ // Ngăn cập nhật vòng tròn: nếu đang syncing state→element, bỏ qua
236
+ if (flags.syncing) {
237
+ return;
238
+ }
239
+
240
+ // Ngăn thao tác push trùng lặp
241
+ if (flags.pushing) {
242
+ return;
243
+ }
244
+
245
+ // Đặt cờ pushing
246
+ flags.pushing = true;
247
+
248
+ try {
249
+ // Lấy giá trị từ element
250
+ const value = OneDOM.getInputValue(element);
251
+
252
+ // Cập nhật state (sẽ kích hoạt state change notifications)
253
+ this.controller.states.__.updateStateAddressKey(stateKey, value);
254
+ } catch (error) {
255
+ logger.error('[BindingManager] Error pushing element to state', error);
256
+ } finally {
257
+ // Xóa cờ async để cho phép hoàn tất cập nhật state
258
+ Promise.resolve().then(() => {
259
+ const currentFlags = this.elementUpdateFlags.get(element);
260
+ if (currentFlags) {
261
+ currentFlags.pushing = false;
262
+ }
263
+ });
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Đồng bộ giá trị state vào element (state → element)
269
+ * Sử dụng trong two-way binding khi state thay đổi
270
+ *
271
+ * Tính năng:
272
+ * - Ngăn cập nhật vòng tròn dùng cờ WeakMap
273
+ * - Chỉ cập nhật nếu giá trị thực sự thay đổi
274
+ * - Validate element đang kết nối
275
+ *
276
+ * @param {HTMLElement} element - Input element
277
+ * @param {string} stateKey - State key (hỗ trợ nested keys như 'user.name')
278
+ * @returns {void}
279
+ *
280
+ * @example
281
+ * // Tự động gọi khi subscribed state thay đổi
282
+ * this.states.subscribe('username', () => {
283
+ * this.syncStateToElement(inputElement, 'username');
284
+ * });
285
+ */
286
+ syncStateToElement(element, stateKey) {
287
+ // Validate element đang kết nối
288
+ if (!element || !element.isConnected) {
289
+ logger.warn('[BindingManager] Cannot sync to disconnected element');
290
+ return;
291
+ }
292
+
293
+ // Lấy hoặc khởi tạo cờ cho element này
294
+ let flags = this.elementUpdateFlags.get(element);
295
+ if (!flags) {
296
+ flags = { pushing: false, syncing: false };
297
+ this.elementUpdateFlags.set(element, flags);
298
+ }
299
+
300
+ // Ngăn cập nhật vòng tròn: nếu đang pushing element→state, bỏ qua
301
+ if (flags.pushing) {
302
+ return;
303
+ }
304
+
305
+ // Ngăn thao tác sync trùng lặp
306
+ if (flags.syncing) {
307
+ return;
308
+ }
309
+
310
+ // Lấy giá trị state
311
+ const stateValue = this.controller.states.__.getStateByAddressKey(stateKey);
312
+ const currentValue = OneDOM.getInputValue(element);
313
+
314
+ // Bỏ qua nếu giá trị không thay đổi (so sánh lỏng để tương thích number/string)
315
+ if (currentValue == stateValue) {
316
+ return;
317
+ }
318
+
319
+ // Đặt cờ syncing
320
+ flags.syncing = true;
321
+
322
+ try {
323
+ // Cập nhật giá trị element
324
+ OneDOM.setInputValue(element, stateValue);
325
+ } catch (error) {
326
+ logger.error('[BindingManager] Error syncing state to element', error);
327
+ } finally {
328
+ // Xóa cờ ngay lập tức (thao tác sync là đồng bộ)
329
+ flags.syncing = false;
330
+ }
331
+ }
332
+
333
+ startClassBindingEventListener() {
334
+ if (this.isClassBindingReadyToListen || !this.classBindingConfigs || this.classBindingConfigs.length === 0) {
335
+ return;
336
+ }
337
+
338
+ const removeClassBindingIDs = [];
339
+ this.classBindingConfigs.forEach(binding => {
340
+ const { id, config, states, initialiClass = '' } = binding;
341
+ let element = null;
342
+ if ((element && !element.isConnected) || element == null || typeof element === 'undefined') {
343
+ const selector = `[data-one-class-id="${id}"]`;
344
+ element = document.querySelector(selector);
345
+ if (element == null) {
346
+ removeClassBindingIDs.push(id);
347
+ logger.warn(`⚠️ BindingManager.startClassBindingListener: No elements found for selector ${selector}`);
348
+ return;
349
+ }
350
+ binding.element = element;
351
+ }
352
+
353
+ this.classBindingListeners.push({ id, states, config, element, initialiClass });
354
+
355
+ // Batch class operations using DOMBatcher
356
+ const classOperations = [];
357
+
358
+ String(initialiClass).split(' ').forEach(cls => {
359
+ if (cls.trim() !== '') {
360
+ classOperations.push({ element, action: 'add', className: cls });
361
+ }
362
+ });
363
+
364
+ Object.entries(config).forEach(([className, classConfig]) => {
365
+ const { states: classStates, checker } = classConfig;
366
+ if (typeof checker !== 'function') {
367
+ return;
368
+ }
369
+ const shouldAdd = checker();
370
+ classOperations.push({
371
+ element,
372
+ action: shouldAdd ? 'add' : 'remove',
373
+ className
374
+ });
375
+ });
376
+
377
+ // Apply all class operations in one batch
378
+ if (classOperations.length > 0) {
379
+ DOMBatcher.write(() => {
380
+ classOperations.forEach(({ element, action, className }) => {
381
+ element.classList[action](className);
382
+ });
383
+ });
384
+ }
385
+ });
386
+
387
+ if (removeClassBindingIDs.length > 0) {
388
+ this.classBindingConfigs = this.classBindingConfigs.filter(binding => !removeClassBindingIDs.includes(binding.id));
389
+ }
390
+
391
+ this.isClassBindingReadyToListen = true;
392
+ }
393
+
394
+ stopClassBindingEventListener() {
395
+ this.isClassBindingReadyToListen = false;
396
+ if (!this.classBindingConfigs || this.classBindingConfigs.length === 0) {
397
+ return;
398
+ }
399
+ }
400
+
401
+ notifyStateChanges(changedKeys) {
402
+ if (this.controller.isRefreshing || !this.controller.isReadyToStateChangeListen) {
403
+ return;
404
+ }
405
+
406
+ if (this.controller.subscribeStates) {
407
+ if (this.controller.subscribeStates === true) {
408
+ this.controller.refresh();
409
+ return;
410
+ }
411
+ if (Array.isArray(this.controller.subscribeStates)) {
412
+ const shouldRefresh = changedKeys.filter(key => this.controller.subscribeStates.includes(key)).length > 0;
413
+ if (shouldRefresh) {
414
+ return this.controller.refresh();
415
+ }
416
+ }
417
+ if (typeof this.controller.subscribeStates === 'function') {
418
+ this.controller.subscribeStates(changedKeys);
419
+ }
420
+ }
421
+
422
+ this.notifyAttributeBindings(changedKeys);
423
+ this.notifyInputBindings(changedKeys);
424
+ this.notifyClassBindings(changedKeys);
425
+
426
+ if (this.controller.children && this.controller.children.length > 0) {
427
+ this.controller.children.forEach(childScope => {
428
+ if (childScope.subscribe && Array.isArray(childScope.subscribe)) {
429
+ const shouldRefresh = changedKeys.some(key => childScope.subscribe.includes(key));
430
+ if (shouldRefresh && childScope.view && childScope.view.__._bindingManager) {
431
+ childScope.view.__._bindingManager.notifyStateChanges(changedKeys);
432
+ }
433
+ }
434
+ });
435
+ }
436
+ }
437
+
438
+ notifyAttributeBindings(changedKeys) {
439
+ if (!Array.isArray(changedKeys) || changedKeys.length === 0) {
440
+ return;
441
+ }
442
+ if (!this.attributeListeners || this.attributeListeners.length === 0) {
443
+ return;
444
+ }
445
+
446
+ const listenersToRemove = [];
447
+
448
+ this.attributeListeners.forEach((listener, index) => {
449
+ const { id, stateKeys, attrs } = listener;
450
+ let element = listener.element;
451
+ if (!element) {
452
+ element = document.querySelector(`[data-one-attribute-id="${id}"]`);
453
+ if (!element) {
454
+ listenersToRemove.push(id);
455
+ return;
456
+ }
457
+ listener.element = element;
458
+ }
459
+
460
+ const shouldUpdate = stateKeys.filter(key => changedKeys.includes(key)).length > 0;
461
+ if (!attrs || !shouldUpdate) {
462
+ return;
463
+ }
464
+
465
+ Object.entries(attrs).forEach(([attrKey, config]) => {
466
+ const { states: stateKeys, render } = config;
467
+ const shouldRender = stateKeys.filter(key => changedKeys.includes(key)).length > 0;
468
+ if (!shouldRender) {
469
+ return;
470
+ }
471
+ if (element && typeof render === 'function') {
472
+ const newValue = render.call(this.controller.view, this.controller.states);
473
+ if(['disabled', 'checked', 'selected'].includes(attrKey)) {
474
+ if (newValue && !['', 'false', '0'].includes(String(newValue).toLowerCase())) {
475
+ element.setAttribute(attrKey, attrKey);
476
+ } else {
477
+ element.removeAttribute(attrKey);
478
+ }
479
+ }
480
+ else if (newValue === null || typeof newValue === 'undefined' || newValue === false) {
481
+ element.removeAttribute(attrKey);
482
+ } else {
483
+ element.setAttribute(attrKey, newValue);
484
+ }
485
+ }
486
+ });
487
+ });
488
+
489
+ this.attributeListeners = this.attributeListeners.filter(listener => !listenersToRemove.includes(listener.id));
490
+ }
491
+
492
+ notifyInputBindings(changedKeys) {
493
+ if (!Array.isArray(changedKeys) || changedKeys.length === 0) {
494
+ return;
495
+ }
496
+ if (this.bindingEventListeners.length === 0) {
497
+ return;
498
+ }
499
+
500
+ // For each binding, check if root key changed
501
+ // and sync using full stateKey (supports nested like 'formData.name')
502
+ this.bindingEventListeners.forEach(({ element, key, stateKey }) => {
503
+ if (changedKeys.includes(key)) {
504
+ this.syncStateToElement(element, stateKey || key);
505
+ }
506
+ });
507
+ }
508
+
509
+ notifyClassBindings(changedKeys) {
510
+ if (!this.isClassBindingReadyToListen) {
511
+ return;
512
+ }
513
+ if (!Array.isArray(changedKeys) || changedKeys.length === 0) {
514
+ return;
515
+ }
516
+ if (!this.classBindingListeners || this.classBindingListeners.length === 0) {
517
+ return;
518
+ }
519
+
520
+ const removeClassBindingIDs = [];
521
+ const classOperations = []; // Batch all class changes
522
+
523
+ this.classBindingListeners.forEach(listener => {
524
+ const { id, states: stateKeys, config, element } = listener;
525
+ if (!element || !(element instanceof Element) || !element.isConnected) {
526
+ removeClassBindingIDs.push(id);
527
+ return;
528
+ }
529
+
530
+ const shouldUpdate = stateKeys.filter(key => changedKeys.includes(key)).length > 0;
531
+ if (!shouldUpdate) {
532
+ return;
533
+ }
534
+
535
+ Object.entries(config).forEach(([className, classConfig]) => {
536
+ const { states: classStates, checker } = classConfig;
537
+ if (classStates.filter(key => changedKeys.includes(key)).length === 0) {
538
+ return;
539
+ }
540
+ if (!(element instanceof Element)) {
541
+ return;
542
+ }
543
+ const shouldAdd = checker();
544
+ classOperations.push({
545
+ element,
546
+ action: shouldAdd ? 'add' : 'remove',
547
+ className
548
+ });
549
+ });
550
+ });
551
+
552
+ // Apply all class operations in one batch to prevent layout thrashing
553
+ if (classOperations.length > 0) {
554
+ DOMBatcher.write(() => {
555
+ classOperations.forEach(({ element, action, className }) => {
556
+ element.classList[action](className);
557
+ });
558
+ });
559
+ }
560
+
561
+ if (removeClassBindingIDs.length > 0) {
562
+ this.classBindingListeners = this.classBindingListeners.filter(listener => !removeClassBindingIDs.includes(listener.id));
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Handle state change event from ViewState
568
+ * Batches changes using microtask queue for optimal performance
569
+ *
570
+ * @param {string} key - State key that changed
571
+ * @param {*} value - New value
572
+ * @param {*} oldValue - Old value
573
+ */
574
+ onStateChange(key, value, oldValue) {
575
+ // Use Set to automatically handle duplicates
576
+ this.controller.changedStateKeys.add(key);
577
+
578
+ // Increment queue count (for tracking/statistics if needed)
579
+ this.controller.changeStateQueueCount++;
580
+
581
+ // Schedule batch processing using Promise for async execution
582
+ // Promise.resolve().then() runs in microtask queue, faster than setTimeout
583
+ // and works even when tab is hidden (unlike requestAnimationFrame)
584
+ // The _stateChangePending flag ensures only one Promise is scheduled at a time
585
+ // All subsequent changes will be batched into the Set
586
+ if (!this.controller._stateChangePending) {
587
+ this.controller._stateChangePending = true;
588
+
589
+ // Use Promise.resolve().then() for microtask queue (fastest, works in background)
590
+ Promise.resolve().then(() => {
591
+ // Clear pending flag before processing
592
+ this.controller._stateChangePending = false;
593
+ this.processStateChanges();
594
+ });
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Process batched state changes efficiently
600
+ * Called from microtask queue after batching period
601
+ * @private
602
+ */
603
+ processStateChanges() {
604
+ // Skip if view is destroyed
605
+ if (this.controller.isDestroyed) {
606
+ return;
607
+ }
608
+
609
+ // Get all changed keys as array for processing
610
+ const changedKeys = Array.from(this.controller.changedStateKeys);
611
+ // Reset collections
612
+ this.controller.changedStateKeys.clear();
613
+ this.controller.changeStateQueueCount = 0;
614
+
615
+ // If no keys changed, skip processing
616
+ if (changedKeys.length === 0) {
617
+ return;
618
+ }
619
+
620
+ // Notify all binding listeners
621
+ this.notifyStateChanges(changedKeys);
622
+ }
623
+
624
+ /**
625
+ * Destroy binding manager and cleanup all resources
626
+ * Prevents memory leaks by removing all event listeners and clearing references
627
+ *
628
+ * Call this in view's beforeDestroy lifecycle
629
+ *
630
+ * @returns {void}
631
+ *
632
+ * @example
633
+ * // In ViewController destroy method
634
+ * beforeDestroy() {
635
+ * this._bindingManager.destroy();
636
+ * }
637
+ */
638
+ destroy() {
639
+ // Stop all event listeners
640
+ this.stopBindingEventListener();
641
+ this.stopClassBindingEventListener();
642
+
643
+ // Clear all arrays and configs
644
+ this.bindingEventListeners = [];
645
+ this.attributeConfigs = [];
646
+ this.attributeListeners = [];
647
+ this.classBindingConfigs = [];
648
+ this.classBindingListeners = [];
649
+
650
+ // WeakMaps will be garbage collected automatically
651
+ // No need to explicitly clear them
652
+
653
+ // Reset flags
654
+ this.isBindingEventListenerStarted = false;
655
+ this.isClassBindingReadyToListen = false;
656
+ this.attributeIndex = 0;
657
+
658
+ // Nullify controller reference to break circular reference
659
+ this.controller = null;
660
+
661
+ logger.log('[BindingManager] Destroyed successfully');
662
+ }
663
+
664
+ get App() {
665
+ return this.controller?.App;
666
+ }
667
+
668
+ set App(value) {
669
+ devLog('BindingManager.App is read-only.');
670
+ }
671
+ }