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,761 @@
1
+ import { __defineProp, __hasOwnProp } from "../helpers/utils.js";
2
+ import logger from "./services/LoggerService.js";
3
+
4
+ /**
5
+ * StateManager - Quản lý state reactive với batched updates
6
+ *
7
+ * Tính năng:
8
+ * - Cập nhật state theo batch dùng requestAnimationFrame
9
+ * - Multi-key subscriptions (kích hoạt một lần khi bất kỳ key nào thay đổi)
10
+ * - Truy cập thuộc tính lồng nhàu (ví dụ: 'user.name')
11
+ * - Phát hiện thay đổi tự động với so sánh sâu
12
+ * - Ngăn memory leak với dọn dẹp đúng cách
13
+ *
14
+ * Sửa Race Condition v2.0.0:
15
+ * - Chiến lược async thống nhất (chỉ RAF, không trộn Promise + RAF)
16
+ * - Hủy RAF đúng cách trong các trường hợp biên
17
+ * - Bảo vệ chống flush đồng thời
18
+ * - Xử lý lỗi tốt hơn với try-catch-finally
19
+ *
20
+ * @class StateManager
21
+ * @since 2.0.0 - Sửa race condition
22
+ */
23
+ export class StateManager {
24
+ constructor(controller, owner) {
25
+ // Thuộc tính private dùng quy ước đặt tên để tương thích trình duyệt
26
+ this.controller = controller;
27
+ this.vs = owner;
28
+
29
+ /**
30
+ * @type {Object<string, {value: any, setValue: function}>}
31
+ */
32
+ this.states = {};
33
+ this.stateIndex = 0;
34
+ this.canUpdateStateByKey = true;
35
+
36
+ this.readyToCommit = false;
37
+
38
+ /**
39
+ * @type {Map<string, Array<Function>>}
40
+ */
41
+ this.listeners = new Map();
42
+
43
+ /**
44
+ * @type {Array<{keys: Set, callback: Function, called: Boolean}>}
45
+ */
46
+ this.multiKeyListeners = [];
47
+
48
+ /**
49
+ * @type {Set<string>}
50
+ */
51
+ this.pendingChanges = new Set();
52
+
53
+ /**
54
+ * @type {boolean}
55
+ * Cờ để ngăn các thao tác flush đồng thời
56
+ */
57
+ this.hasPendingFlush = false;
58
+
59
+ /**
60
+ * @type {boolean}
61
+ * Cờ để ngăn các lời gọi flush tái nhập
62
+ */
63
+ this.isFlushing = false;
64
+
65
+ /**
66
+ * @type {number|null}
67
+ * RAF ID để hủy và dọn dẹp
68
+ */
69
+ this.flushRAF = null;
70
+
71
+ /**
72
+ * @type {Array<string>}
73
+ */
74
+ this.ownProperties = ['__'];
75
+
76
+ /**
77
+ * @type {Array<string>}
78
+ */
79
+ this.ownMethods = ['on', 'off'];
80
+
81
+ this.setters = {};
82
+
83
+ /**
84
+ * @type {WeakMap}
85
+ * Cho dữ liệu cục bộ component (tự động garbage collected)
86
+ */
87
+ this._scopedData = new WeakMap();
88
+
89
+ /**
90
+ * @type {number}
91
+ * Theo dõi số lượng thao tác flush để debug
92
+ */
93
+ this._flushCount = 0;
94
+
95
+ /**
96
+ * @type {boolean}
97
+ * Cờ chỉ StateManager đã bị hủy
98
+ */
99
+ this._isDestroyed = false;
100
+ }
101
+
102
+ // Define public methods using Object.defineProperties for backward compatibility
103
+
104
+ /**
105
+ * Commit thay đổi state vào batch queue
106
+ *
107
+ * Sửa Race Condition:
108
+ * - Chỉ dùng RAF (không trộn Promise + RAF)
109
+ * - Quản lý cờ đúng cách
110
+ * - An toàn khi hủy
111
+ *
112
+ * @param {string} key - State key đã thay đổi
113
+ * @param {*} oldValue - Giá trị trước đó
114
+ * @returns {boolean} False nếu giá trị không thay đổi, undefined nếu không
115
+ *
116
+ * @private
117
+ */
118
+ commitStateChange(key, oldValue) {
119
+ // Kiểm tra đã bị hủy chưa
120
+ if (this._isDestroyed) {
121
+ logger.warn('[StateManager] Cannot commit state change - manager is destroyed');
122
+ return false;
123
+ }
124
+
125
+ // Kiểm tra sẵn sàng commit
126
+ if (!this.readyToCommit) {
127
+ return;
128
+ }
129
+
130
+ // So sánh nhanh cho primitives
131
+ const newValue = this.getStateByAddressKey(key);
132
+
133
+ // So sánh sâu cho objects/arrays
134
+ if (typeof oldValue === 'object' || typeof newValue === 'object') {
135
+ if (this.parseCompareValue(oldValue) === this.parseCompareValue(newValue)) {
136
+ return false;
137
+ }
138
+ }
139
+ else if (oldValue === newValue) {
140
+ return false;
141
+ }
142
+
143
+ // Thêm vào pending changes
144
+ this.pendingChanges.add(key);
145
+
146
+ // Lên lịch flush nếu chưa được lên lịch
147
+ // Chỉ dùng requestAnimationFrame để nhất quán
148
+ // Điều này ngăn race conditions giữa Promise microtasks và RAF macrotasks
149
+ if (!this.hasPendingFlush) {
150
+ this.hasPendingFlush = true;
151
+
152
+ try {
153
+ this.flushRAF = requestAnimationFrame(() => {
154
+ this._executeFlush();
155
+ });
156
+ } catch (error) {
157
+ logger.error('[StateManager] Error scheduling flush:', error);
158
+ this.hasPendingFlush = false;
159
+ this.flushRAF = null;
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Thực thi thao tác flush với xử lý lỗi đúng cách
166
+ * Bao bọc flushChanges() với các kiểm tra an toàn
167
+ *
168
+ * @private
169
+ */
170
+ _executeFlush() {
171
+ // Kiểm tra đã bị hủy chưa
172
+ if (this._isDestroyed) {
173
+ return;
174
+ }
175
+
176
+ // Ngăn các lời gọi tái nhập
177
+ if (this.isFlushing) {
178
+ logger.warn('[StateManager] Flush already in progress, skipping');
179
+ return;
180
+ }
181
+
182
+ try {
183
+ this.isFlushing = true;
184
+ this.flushChanges();
185
+ } catch (error) {
186
+ logger.error('[StateManager] Error during flush:', error);
187
+ } finally {
188
+ // Luôn xóa cờ ngay cả khi có lỗi xảy ra
189
+ this.isFlushing = false;
190
+ this.hasPendingFlush = false;
191
+ this.flushRAF = null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Flush tất cả các thay đổi state đang chờ vào listeners
197
+ * Xử lý batch tất cả thay đổi đã tích lũy trong một thao tác
198
+ *
199
+ * @private
200
+ */
201
+ flushChanges() {
202
+ // Kiểm tra điều kiện lần nữa
203
+ if (this.pendingChanges.size === 0) {
204
+ return;
205
+ }
206
+
207
+ // Tăng bộ đếm flush để debug
208
+ this._flushCount++;
209
+
210
+ // Batch tất cả thay đổi cho chu kỳ flush này
211
+ const changesToProcess = Array.from(this.pendingChanges);
212
+ this.pendingChanges.clear();
213
+
214
+ if (changesToProcess.length === 0) {
215
+ return;
216
+ }
217
+
218
+ // Log để debug (có thể tắt trong production)
219
+ if (this.controller?.App?.env?.debug) {
220
+ logger.log(`[StateManager] Flushing ${changesToProcess.length} changes (flush #${this._flushCount}):`, changesToProcess);
221
+ }
222
+
223
+ // Reset cờ called của multi-key listeners
224
+ for (const listener of this.multiKeyListeners) {
225
+ listener.called = false;
226
+ }
227
+
228
+ // Kích hoạt single-key listeners
229
+ for (const changedKey of changesToProcess) {
230
+ const listeners = this.listeners.get(changedKey);
231
+ if (listeners && listeners.length > 0) {
232
+ const currentValue = this.states[changedKey]?.value;
233
+ // Dùng for loop để hiệu suất tốt hơn
234
+ for (let i = 0; i < listeners.length; i++) {
235
+ try {
236
+ listeners[i](currentValue);
237
+ } catch (error) {
238
+ logger.error('[StateManager] Listener error:', error, {
239
+ key: changedKey,
240
+ listenerIndex: i
241
+ });
242
+ }
243
+ }
244
+ }
245
+
246
+ // Kiểm tra multi-key listeners
247
+ for (const listener of this.multiKeyListeners) {
248
+ if (!listener.called && listener.keys.has(changedKey)) {
249
+ listener.called = true;
250
+
251
+ // Thu thập tất cả giá trị đã thay đổi cho các keys đăng ký
252
+ const values = {};
253
+ for (const k of listener.keys) {
254
+ if (changesToProcess.includes(k)) {
255
+ values[k] = this.states[k]?.value;
256
+ }
257
+ }
258
+
259
+ // Validate callback trước khi gọi
260
+ if (typeof listener.callback === 'function') {
261
+ try {
262
+ listener.callback(values);
263
+ } catch (error) {
264
+ logger.error('[StateManager] Multi-key listener error:', error, {
265
+ keys: Array.from(listener.keys)
266
+ });
267
+ }
268
+ } else {
269
+ logger.error('[StateManager] listener.callback is not a function', {
270
+ keys: Array.from(listener.keys),
271
+ callbackType: typeof listener.callback
272
+ });
273
+ }
274
+ }
275
+ }
276
+
277
+ // Kích hoạt onStateChange của view
278
+ if (this.controller && typeof this.controller._bindingManager?.onStateChange === 'function') {
279
+ try {
280
+ this.controller._bindingManager.onStateChange(changedKey, this.states[changedKey]?.value);
281
+ } catch (error) {
282
+ logger.error('[StateManager] onStateChange error:', error, { key: changedKey });
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * set state nội bộ
290
+ * @param {number} index index
291
+ * @param {*} value giá trị
292
+ * @param {function} setValue hàm set giá trị
293
+ * @param {string} key key của state
294
+ * @returns {[*, function, string]}
295
+ */
296
+ setState(key, value, setValue = () => { }) {
297
+ if (this.states[key]) {
298
+ return [value, setValue, key];
299
+ }
300
+ this.states[key] = {
301
+ value: value,
302
+ setValue: setValue,
303
+ key: key,
304
+ };
305
+ return [value, setValue, key];
306
+ }
307
+
308
+ /**
309
+ * tương tự useState
310
+ * @param {*} value giá trị của state
311
+ * @param {string} key key của state (không bắt buộc)
312
+ * @returns {Array<[*, function, string]>}
313
+ */
314
+ useState(value, key = null) {
315
+ if (key && __hasOwnProp(this.states, key)) {
316
+ return [this.states[key].value, this.states[key].setValue, key];
317
+ }
318
+ const index = this.stateIndex++;
319
+ const stateKey = key ?? index;
320
+ const setValue = (value) => {
321
+ const oldValue = this.states[stateKey].value;
322
+ this.states[stateKey].value = value;
323
+ this.commitStateChange(stateKey, oldValue);
324
+ };
325
+ this.setState(stateKey, value, setValue);
326
+
327
+ if (!this.ownProperties.includes(stateKey) && !this.ownMethods.includes(stateKey)) {
328
+ const $self = this;
329
+ Object.defineProperty(this.vs, stateKey, {
330
+ get: () => {
331
+ return $self.states[stateKey].value;
332
+ },
333
+ set: (value) => {
334
+ if (typeof $self.setters[stateKey] === 'function') {
335
+ return $self.setters[stateKey](value);
336
+ }
337
+ else {
338
+ logger.log("Bạn không thể thiết lập giá trị cho " + stateKey + " theo cách này");
339
+ }
340
+ },
341
+ configurable: false,
342
+ enumerable: true,
343
+ });
344
+
345
+ }
346
+ return [value, setValue, stateKey];
347
+ }
348
+
349
+ /**
350
+ * cập nhật state value theo key
351
+ * @param {string} key key của state
352
+ * @param {*} value giá trị
353
+ * @returns {*}
354
+ */
355
+ updateStateByKey(key, value) {
356
+ if (!this.states[key]) {
357
+ return;
358
+ }
359
+ if (!this.canUpdateStateByKey) {
360
+ return this.states[key].value;
361
+ }
362
+ const oldValue = this.states[key].value;
363
+ this.states[key].value = value;
364
+ this.commitStateChange(key, oldValue);
365
+ return value;
366
+ }
367
+
368
+ updateStateAddressKey(key, value) {
369
+ const keyPaths = key.split('.');
370
+ const _key = keyPaths.shift();
371
+ if (!this.states[_key]) {
372
+ return;
373
+ }
374
+ let stateValue = this.states[_key].value;
375
+ if (keyPaths.length === 0 || typeof stateValue !== 'object' || stateValue === null) {
376
+ return this.setters[_key](value);
377
+ }
378
+
379
+ // Clone object/array to create new reference for reactivity
380
+ // This ensures oldValue !== newValue in commitStateChange
381
+ let clonedValue;
382
+ if (Array.isArray(stateValue)) {
383
+ clonedValue = [...stateValue];
384
+ } else {
385
+ clonedValue = { ...stateValue };
386
+ }
387
+
388
+ let current = clonedValue;
389
+ for (let i = 0; i < keyPaths.length - 1; i++) {
390
+ const path = keyPaths[i];
391
+ if (typeof current[path] !== 'object' || current[path] === null) {
392
+ current[path] = {};
393
+ } else {
394
+ // Clone nested objects/arrays
395
+ current[path] = Array.isArray(current[path]) ? [...current[path]] : { ...current[path] };
396
+ }
397
+ current = current[path];
398
+ }
399
+ const lastPath = keyPaths[keyPaths.length - 1];
400
+ current[lastPath] = value;
401
+ return this.setters[_key](clonedValue);
402
+ }
403
+
404
+ getStateByAddressKey(key) {
405
+ // Convert key to string if it's a number
406
+ const keyString = String(key);
407
+
408
+ // Fast path for simple keys (no dots)
409
+ if(!keyString.includes('.')) {
410
+ return this.states[keyString]?.value ?? null;
411
+ }
412
+
413
+ const keyPaths = keyString.split('.');
414
+ const rootKey = keyPaths[0];
415
+
416
+ if (!this.states[rootKey]) {
417
+ return null;
418
+ }
419
+
420
+ let current = this.states[rootKey].value;
421
+
422
+ // Early return for root level
423
+ if (keyPaths.length === 1) {
424
+ return current;
425
+ }
426
+
427
+ // Traverse nested path
428
+ for (let i = 1; i < keyPaths.length; i++) {
429
+ if (typeof current !== 'object' || current === null) {
430
+ return null;
431
+ }
432
+ current = current[keyPaths[i]];
433
+ if (current === undefined) {
434
+ return null;
435
+ }
436
+ }
437
+
438
+ return current;
439
+ }
440
+
441
+ /**
442
+ * đăng ký key - value cho state - trả về hàm setValue cho key tương ứng
443
+ * @param {string} key key của state
444
+ * @param {*} value giá trị
445
+ * @returns {function}
446
+ */
447
+ register(key, value) {
448
+ return this.useState(value, key)[1];
449
+ }
450
+
451
+ lockUpdateRealState() {
452
+ this.canUpdateStateByKey = false;
453
+ }
454
+
455
+ subscribe(key, callback) {
456
+ // Support array keys: subscribe(['key1', 'key2'], callback)
457
+ // Callback will be called once when any key changes
458
+ if(Array.isArray(key)){
459
+ // Validate callback
460
+ if(key.length === 0) {
461
+ return () => {};
462
+ }
463
+ if(key.length === 1){
464
+ // Single key in array, redirect to single key subscription
465
+ return this.subscribe(key[0], callback);
466
+ }
467
+ if(typeof callback !== 'function'){
468
+ logger.error('ViewState.subscribe: callback must be a function for array keys', {
469
+ keys: key,
470
+ callbackType: typeof callback
471
+ });
472
+ return () => {};
473
+ }
474
+
475
+ const keys = new Set();
476
+ for(const k of key) {
477
+ if(typeof k === 'string' &&
478
+ this.states[k] &&
479
+ !this.ownProperties.includes(k) &&
480
+ !this.ownMethods.includes(k)) {
481
+ keys.add(k);
482
+ }
483
+ }
484
+
485
+ if(keys.size === 0){
486
+ return () => {};
487
+ }
488
+
489
+ const listener = { keys, callback, called: false };
490
+ this.multiKeyListeners.push(listener);
491
+
492
+ // Return unsubscribe function
493
+ return () => {
494
+ const index = this.multiKeyListeners.indexOf(listener);
495
+ if(index !== -1){
496
+ this.multiKeyListeners.splice(index, 1);
497
+ }
498
+ };
499
+ }
500
+
501
+ // Support object keys: subscribe({key1: cb1, key2: cb2})
502
+ if(typeof key === "object" && key !== null){
503
+ const unsubscribes = {};
504
+ for(const k of Object.keys(key)){
505
+ unsubscribes[k] = this.subscribe(k, key[k]);
506
+ }
507
+ return () => {
508
+ for(const k of Object.keys(unsubscribes)){
509
+ unsubscribes[k]();
510
+ }
511
+ };
512
+ }
513
+
514
+ // Support space-separated string keys: subscribe("key1 key2", callback)
515
+ if(typeof key === 'string' && key.includes(' ')){
516
+ const keyArray = key.split(/\s+/).filter(k => k.length > 0);
517
+ return this.subscribe(keyArray, callback);
518
+ }
519
+
520
+ // Single key subscription
521
+ if (typeof key !== 'string' || this.ownProperties.includes(key) || this.ownMethods.includes(key)) {
522
+ return () => {};
523
+ }
524
+
525
+ if (typeof callback !== 'function') {
526
+ throw new Error('Callback must be a function');
527
+ }
528
+ if (!this.listeners.has(key)) {
529
+ this.listeners.set(key, []);
530
+ }
531
+ this.listeners.get(key).push(callback);
532
+
533
+ let index = this.listeners.get(key).length - 1;
534
+ return () => {
535
+ this.listeners.get(key).splice(index, 1);
536
+ if (this.listeners.get(key).length === 0) {
537
+ this.listeners.delete(key);
538
+ }
539
+ };
540
+ }
541
+
542
+ unsubscribe(key, callback = null) {
543
+ // Support array keys (order-independent)
544
+ if(Array.isArray(key)){
545
+ if(key.length == 0){
546
+ return;
547
+ }
548
+
549
+ if(key.length == 1){
550
+ // Single key in array, redirect to single key unsubscription
551
+ this.unsubscribe(key[0], callback);
552
+ return;
553
+ }
554
+
555
+ const keySet = new Set(key);
556
+
557
+ // Helper function to check if two Sets are equal
558
+ const areSetsEqual = (set1, set2) => {
559
+ if(set1.size !== set2.size) return false;
560
+ for(const k of set1){
561
+ if(!set2.has(k)) return false;
562
+ }
563
+ return true;
564
+ };
565
+
566
+ if(!callback){
567
+ // Remove ALL multi-key listeners with same key set
568
+ for(let i = this.multiKeyListeners.length - 1; i >= 0; i--){
569
+ if(areSetsEqual(this.multiKeyListeners[i].keys, keySet)){
570
+ this.multiKeyListeners.splice(i, 1);
571
+ }
572
+ }
573
+ return;
574
+ }
575
+
576
+ // Remove specific listener with callback
577
+ const index = this.multiKeyListeners.findIndex(listener => {
578
+ return listener.callback === callback && areSetsEqual(listener.keys, keySet);
579
+ });
580
+
581
+ if(index !== -1){
582
+ this.multiKeyListeners.splice(index, 1);
583
+ }
584
+ return;
585
+ }
586
+
587
+ // Support object keys
588
+ if(typeof key === "object" && key !== null){
589
+ for(const k of Object.keys(key)){
590
+ this.unsubscribe(k, key[k]);
591
+ }
592
+ return;
593
+ }
594
+
595
+ // Single key unsubscription
596
+ if (typeof key !== 'string' || this.ownProperties.includes(key) || this.ownMethods.includes(key)) {
597
+ return;
598
+ }
599
+ if (callback && typeof callback !== 'function') {
600
+ return;
601
+ }
602
+ if (callback) {
603
+ const listeners = this.listeners.get(key);
604
+ if(listeners){
605
+ let index = listeners.indexOf(callback);
606
+ if (index !== -1) {
607
+ listeners.splice(index, 1);
608
+ if (listeners.length === 0) {
609
+ this.listeners.delete(key);
610
+ }
611
+ }
612
+ }
613
+ } else {
614
+ this.listeners.delete(key);
615
+ }
616
+ }
617
+
618
+ on(key, callback) {
619
+ return this.subscribe(key, callback);
620
+ }
621
+
622
+ off(key, callback = null) {
623
+ this.unsubscribe(key, callback);
624
+ }
625
+
626
+ parseCompareValue(value) {
627
+ if (value === null || typeof value === 'undefined') {
628
+ return value;
629
+ }
630
+ if (typeof value === 'object') {
631
+ let isArray = Array.isArray(value);
632
+ let data = isArray ? [] : {};
633
+ let d = isArray ? value.forEach(v => data.push(this.parseCompareValue(v))) : Object.entries(value).forEach(([k, v]) => data[k] = this.parseCompareValue(v));
634
+ return JSON.stringify(data);
635
+ }
636
+ return value;
637
+ }
638
+
639
+ /**
640
+ * Clean up all listeners and pending operations
641
+ * Prevents memory leaks by properly cancelling RAF and clearing references
642
+ *
643
+ * Call this when destroying a view
644
+ *
645
+ * @example
646
+ * // In ViewController beforeDestroy
647
+ * beforeDestroy() {
648
+ * this.states.__?.destroy();
649
+ * }
650
+ */
651
+ destroy() {
652
+ // Mark as destroyed to prevent new operations
653
+ this._isDestroyed = true;
654
+
655
+ // Cancel pending flush operation
656
+ if (this.flushRAF !== null) {
657
+ try {
658
+ cancelAnimationFrame(this.flushRAF);
659
+ } catch (error) {
660
+ logger.error('[StateManager] Error cancelling RAF:', error);
661
+ }
662
+ this.flushRAF = null;
663
+ }
664
+
665
+ // Clear all listeners
666
+ this.listeners.clear();
667
+ this.multiKeyListeners = [];
668
+ this.pendingChanges.clear();
669
+
670
+ // Reset all flags
671
+ this.hasPendingFlush = false;
672
+ this.isFlushing = false;
673
+ this.readyToCommit = false;
674
+
675
+ // Clear states
676
+ this.states = {};
677
+ this.setters = {};
678
+
679
+ // WeakMap will be garbage collected automatically
680
+ this._scopedData = new WeakMap();
681
+
682
+ // Nullify controller reference to break circular reference
683
+ this.controller = null;
684
+
685
+ logger.log('[StateManager] Destroyed successfully');
686
+ }
687
+
688
+ /**
689
+ * Reset state manager to initial state
690
+ * Keeps structure but clears all data
691
+ *
692
+ * Use when you want to reuse the manager with fresh state
693
+ */
694
+ reset() {
695
+ // Cancel pending flush
696
+ if (this.flushRAF !== null) {
697
+ try {
698
+ cancelAnimationFrame(this.flushRAF);
699
+ } catch (error) {
700
+ logger.error('[StateManager] Error cancelling RAF during reset:', error);
701
+ }
702
+ this.flushRAF = null;
703
+ }
704
+
705
+ // Reset state tracking
706
+ this.stateIndex = 0;
707
+ this.canUpdateStateByKey = true;
708
+
709
+ // Clear listeners and pending changes
710
+ this.listeners.clear();
711
+ this.multiKeyListeners = [];
712
+ this.pendingChanges.clear();
713
+
714
+ // Reset flags
715
+ this.hasPendingFlush = false;
716
+ this.isFlushing = false;
717
+
718
+ // Reset counters
719
+ this._flushCount = 0;
720
+
721
+ logger.log('[StateManager] Reset successfully');
722
+ }
723
+
724
+ toJSON() {
725
+ const data = {};
726
+ Object.entries(this.states).forEach(([key, state]) => {
727
+ data[key] = state.value;
728
+ });
729
+ return data;
730
+ }
731
+
732
+ toString() {
733
+ return JSON.stringify(this.toJSON());
734
+ }
735
+
736
+ }
737
+
738
+ export class ViewState {
739
+ constructor(view) {
740
+ const manager = new StateManager(view, this);
741
+ __defineProp(this, '__', {
742
+ value: manager,
743
+ writable: false,
744
+ configurable: false,
745
+ enumerable: false,
746
+ });
747
+ }
748
+
749
+ /**
750
+ * @returns {StateManager}
751
+ */
752
+ toJSON() {
753
+ return this.__.toJSON();
754
+ }
755
+
756
+ toString() {
757
+ return this.__.toString();
758
+ }
759
+
760
+ }
761
+