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.
- package/README.md +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- 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
|
+
|