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,592 @@
1
+ import { uniqId } from "../../helpers/utils.js";
2
+ import OneMarkup, { OneMarkupModel } from "../OneMarkup.js";
3
+ import OneDOM from "../OneDOM.js";
4
+ import { ViewState } from "../ViewState.js";
5
+
6
+ /**
7
+ * ReactiveComponent - Reactive component thống nhất
8
+ * Thay thế cả WatchComponent và OutputComponent để hiệu suất và bảo trì tốt hơn
9
+ *
10
+ * @class ReactiveComponent
11
+ * @description Xử lý reactive rendering cho cả watch blocks và output expressions
12
+ *
13
+ * Tính năng:
14
+ * - Quản lý lifecycle thống nhất
15
+ * - Tối ưu state subscriptions
16
+ * - Xử lý children view tốt hơn
17
+ * - Hỗ trợ escape HTML cho loại output
18
+ * - Xử lý lỗi cải thiện
19
+ * - Ngăn memory leak
20
+ */
21
+ export class ReactiveComponent {
22
+ /**
23
+ * @param {Object} options
24
+ * @param {Application} options.App - Instance Application
25
+ * @param {ViewController} options.controller - View controller
26
+ * @param {Array<string>} options.stateKeys - Các state keys để theo dõi
27
+ * @param {Function} options.renderBlock - Hàm render
28
+ * @param {string} options.renderID - Component ID
29
+ * @param {ReactiveComponent} options.parentWatchComponent - Component cha
30
+ * @param {string} options.type - Loại component: 'watch' hoặc 'output'
31
+ * @param {boolean} options.escapeHTML - Escape HTML cho output
32
+ */
33
+ constructor({
34
+ App,
35
+ controller,
36
+ stateKeys = [],
37
+ renderBlock = () => '',
38
+ renderID = '',
39
+ parentWatchComponent = null,
40
+ type = 'watch',
41
+ escapeHTML = false
42
+ }) {
43
+ /**
44
+ * @type {Application}
45
+ */
46
+ this.App = App;
47
+ /**
48
+ * @type {ViewController}
49
+ */
50
+ this.controller = controller;
51
+ /**
52
+ * @type {ViewState}
53
+ */
54
+ this.states = controller.states;
55
+ /**
56
+ * @type {Array<string>}
57
+ */
58
+ this.stateKeys = stateKeys;
59
+ /**
60
+ * @type {Function}
61
+ */
62
+ this.renderBlock = renderBlock;
63
+ /**
64
+ * @type {string}
65
+ */
66
+ this.id = renderID || uniqId();
67
+ /**
68
+ * @type {string} - 'watch' hoặc 'output'
69
+ */
70
+ this.type = type;
71
+ /**
72
+ * @type {boolean}
73
+ */
74
+ this.escapeHTML = escapeHTML;
75
+
76
+ // Các cờ lifecycle
77
+ this.isMounted = false;
78
+ this.isScanned = false;
79
+ this.isDestroyed = false;
80
+ this._isUpdating = false;
81
+
82
+ // Tham chiếu DOM
83
+ this.openTag = null;
84
+ this.closeTag = null;
85
+ this.refElements = [];
86
+ /**
87
+ * @type {OneMarkupModel|null}
88
+ */
89
+ this.markup = null;
90
+
91
+ // Quản lý state
92
+ this.subscribes = [];
93
+ this.renderedContent = '';
94
+
95
+ // Cấp bậc
96
+ this.parentWatchComponent = parentWatchComponent;
97
+ this.childrenIDs = [];
98
+ }
99
+
100
+ /**
101
+ * Mount component và thiết lập state subscriptions
102
+ */
103
+ mounted() {
104
+ if (this.isMounted || this.isDestroyed) return;
105
+
106
+ // console.log(`ReactiveComponent mounted [${this.type}]:`, this.id);
107
+
108
+ this.scan();
109
+
110
+ // Đăng ký theo dõi state changes
111
+ if (this.stateKeys.length > 0) {
112
+ const unsubscribe = this.states.__.subscribe(this.stateKeys, () => {
113
+ if (!this._isUpdating && this.isMounted) {
114
+ // console.log(`ReactiveComponent state changed [${this.type}], updating:`, this.id);
115
+ this.update();
116
+ }
117
+ });
118
+ this.subscribes.push(unsubscribe);
119
+ }
120
+
121
+ this.isMounted = true;
122
+
123
+ // Mount children cho loại watch
124
+ if (this.type === 'watch') {
125
+ this._mountChildren();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Unmount component và dọn dẹp subscriptions
131
+ */
132
+ unmounted() {
133
+ if (!this.isMounted || this.isDestroyed) return;
134
+
135
+ // console.log(`ReactiveComponent unmounted [${this.type}]:`, this.id);
136
+
137
+ this.isMounted = false;
138
+
139
+ // Unmount children cho loại watch
140
+ if (this.type === 'watch') {
141
+ this._unmountChildren();
142
+ }
143
+
144
+ this._unsubscribeAll();
145
+ }
146
+
147
+ /**
148
+ * Cập nhật component khi state thay đổi
149
+ */
150
+ update() {
151
+ if (this._isUpdating || !this.isMounted || this.isDestroyed) return;
152
+
153
+ this._isUpdating = true;
154
+
155
+ try {
156
+ this.controller._reactiveManager?.onWatchComponentUpdating?.();
157
+
158
+ if (this.type === 'watch') {
159
+ // Cập nhật đầy đủ cho watch components với quản lý children
160
+ this._updateWatchComponent();
161
+ } else {
162
+ // Cập nhật nhanh cho output components (không có children)
163
+ this._updateOutputComponent();
164
+ }
165
+
166
+ this.controller._reactiveManager?.onWatchComponentUpdated?.();
167
+ } catch (error) {
168
+ console.error(`ReactiveComponent update error [${this.type}][${this.id}]:`, error);
169
+ } finally {
170
+ this._isUpdating = false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Cập nhật watch component với quản lý children
176
+ * @private
177
+ */
178
+ _updateWatchComponent() {
179
+ // Lưu context gốc
180
+ const originalChildrenIDs = this.controller._hierarchyManager?.renewChildrenIDs || [];
181
+ const originRCChildrenIDs = this.controller._hierarchyManager?.rcChildrenIDs || [];
182
+
183
+ if (this.controller._hierarchyManager) {
184
+ this.controller._hierarchyManager.renewChildrenIDs = this.childrenIDs;
185
+ this.controller._hierarchyManager.rcChildrenIDs = [];
186
+ }
187
+
188
+ try {
189
+ // Unmount và xóa
190
+ this._unmountChildren();
191
+ this.clear();
192
+
193
+ // Render lại
194
+ if (this.closeTag?.parentNode) {
195
+ const newContent = this.renderContent();
196
+ OneDOM.before(this.closeTag, newContent);
197
+
198
+ // Cập nhật children IDs
199
+ if (this.controller._hierarchyManager) {
200
+ this.childrenIDs = this.controller._hierarchyManager.rcChildrenIDs.slice();
201
+ }
202
+
203
+ // Re-scan and mount
204
+ this.scan();
205
+ this._mountChildren();
206
+ }
207
+ } finally {
208
+ // Restore context
209
+ if (this.controller._hierarchyManager) {
210
+ this.controller._hierarchyManager.renewChildrenIDs = originalChildrenIDs;
211
+ this.controller._hierarchyManager.rcChildrenIDs = originRCChildrenIDs;
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Quick update for output component (no children management)
218
+ * @private
219
+ */
220
+ _updateOutputComponent() {
221
+ this.clear();
222
+
223
+ if (this.closeTag?.parentNode) {
224
+ const newContent = this.renderContent();
225
+
226
+ // Output component uses text node for performance
227
+ const textNode = document.createTextNode(newContent);
228
+ OneDOM.before(this.closeTag, textNode);
229
+
230
+ this.scan();
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Render content with error handling and escaping
236
+ */
237
+ renderContent() {
238
+ try {
239
+ let content = this.renderBlock(this);
240
+
241
+ // Apply HTML escaping for output type
242
+ if (this.type === 'output' && this.escapeHTML) {
243
+ if (typeof content === 'string') {
244
+ content = this.App.View.escString(content);
245
+ } else if (content != null) {
246
+ content = this.App.View.escString(String(content));
247
+ }
248
+ }
249
+
250
+ this.renderedContent = content ?? '';
251
+ return this.renderedContent;
252
+ } catch (error) {
253
+ console.error(`ReactiveComponent renderContent error [${this.type}][${this.id}]:`, error);
254
+ this.renderedContent = '';
255
+ return this.type === 'watch'
256
+ ? `<!-- Error in ${this.type}: ${error.message} -->`
257
+ : '';
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Tạo output render đầy đủ với comment markers
263
+ */
264
+ render() {
265
+ const content = this.renderContent();
266
+
267
+ // Dùng tên tag khác nhau cho các loại khác nhau
268
+ if (this.type === 'output') {
269
+ return `<!-- [one:reactive-out id="${this.id}"] -->${content}<!-- [/one:reactive-out] -->`;
270
+ }
271
+
272
+ return `<!-- [one:reactive id="${this.id}"] -->${content}<!-- [/one:reactive] -->`;
273
+ }
274
+
275
+ /**
276
+ * Quét và cache các tham chiếu DOM giữa comment markers
277
+ *
278
+ * v2.0.0 - Cải thiện validation:
279
+ * - Kiểm tra kết nối markup trước khi quét
280
+ * - Validate sự tồn tại của openTag/closeTag
281
+ * - Xử lý lỗi và logging tốt hơn
282
+ */
283
+ scan() {
284
+ if (this.isScanned || this.isDestroyed) return;
285
+
286
+ try {
287
+ if (!this.markup) {
288
+ // Thử reactive markup mới trước
289
+ const tagName = this.type === 'output' ? 'reactive-out' : 'reactive';
290
+ this.markup = OneMarkup.first(tagName, { id: this.id }, { useCache: false });
291
+
292
+ // Fallback về legacy markup để tương thích ngược
293
+ if (!this.markup) {
294
+ const legacyTag = this.type === 'output' ? 'output' : 'watch';
295
+ this.markup = OneMarkup.first(legacyTag, { id: this.id }, { useCache: false });
296
+ }
297
+
298
+ if (!this.markup) {
299
+ this.isScanned = true;
300
+ return;
301
+ }
302
+
303
+ // Validate markup có các tags cần thiết
304
+ if (!this.markup.openTag || !this.markup.closeTag) {
305
+ console.warn(`ReactiveComponent [${this.type}][${this.id}]: Markup missing tags`);
306
+ this.isScanned = true;
307
+ return;
308
+ }
309
+
310
+ // Validate các tags vẫn còn trong DOM
311
+ if (!this.markup.openTag.isConnected || !this.markup.closeTag.isConnected) {
312
+ console.warn(`ReactiveComponent [${this.type}][${this.id}]: Markup tags not connected`);
313
+ this.isScanned = true;
314
+ return;
315
+ }
316
+
317
+ this.openTag = this.markup.openTag;
318
+ this.closeTag = this.markup.closeTag;
319
+ this.refElements = this.markup.nodes.slice();
320
+ } else {
321
+ // Quét lại markup hiện tại - validate trước
322
+ if (!this.markup.openTag?.isConnected || !this.markup.closeTag?.isConnected) {
323
+ console.warn(`ReactiveComponent [${this.type}][${this.id}]: Cannot re-scan, markup disconnected`);
324
+ this.isScanned = true;
325
+ return;
326
+ }
327
+
328
+ this.refElements = [];
329
+ this.markup.__scan();
330
+ this.refElements = this.markup.nodes.slice();
331
+ }
332
+
333
+ this.isScanned = true;
334
+ } catch (error) {
335
+ console.error(`ReactiveComponent scan error [${this.type}][${this.id}]:`, error);
336
+ this.isScanned = true;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Xóa các DOM elements giữa markers
342
+ *
343
+ * v2.0.0 - Cải thiện dọn dẹp:
344
+ * - Validates kết nối node trước khi xóa
345
+ * - Xóa mảng sau khi xóa
346
+ * - Ngăn memory leaks từ các DOM refs bị bỏ rơi
347
+ */
348
+ clear() {
349
+ this.isScanned = false;
350
+
351
+ // Remove all DOM nodes between markers
352
+ this.refElements.forEach(node => {
353
+ try {
354
+ // Only remove if node is still in DOM
355
+ if (node && node.parentNode && node.isConnected) {
356
+ node.parentNode.removeChild(node);
357
+ }
358
+ } catch (error) {
359
+ console.error(`ReactiveComponent clear node error [${this.type}]:`, error);
360
+ }
361
+ });
362
+
363
+ // Clear array to release references
364
+ this.refElements.length = 0;
365
+ }
366
+
367
+ /**
368
+ * Destroy component completely
369
+ *
370
+ * v2.0.0 - Enhanced memory leak prevention:
371
+ * - Proper OneMarkup disposal
372
+ * - Break circular references
373
+ * - Clear all arrays
374
+ * - Cleanup DOM references
375
+ */
376
+ destroy() {
377
+ if (this.isDestroyed) return;
378
+
379
+ console.log(`ReactiveComponent destroyed [${this.type}]:`, this.id);
380
+
381
+ // Unmount first
382
+ this.unmounted();
383
+
384
+ // Clear DOM elements
385
+ this.clear();
386
+
387
+ // Dispose OneMarkup (clear internal cache)
388
+ if (this.markup && typeof this.markup.dispose === 'function') {
389
+ try {
390
+ this.markup.dispose();
391
+ } catch (error) {
392
+ console.error(`ReactiveComponent markup disposal error [${this.type}]:`, error);
393
+ }
394
+ }
395
+
396
+ // Clear all arrays to release references
397
+ this.refElements = [];
398
+ this.childrenIDs = [];
399
+ this.stateKeys = [];
400
+
401
+ // Break circular reference with parent
402
+ if (this.parentWatchComponent) {
403
+ // Remove this component from parent's children
404
+ if (this.parentWatchComponent.childrenIDs) {
405
+ const index = this.parentWatchComponent.childrenIDs.indexOf(this.id);
406
+ if (index > -1) {
407
+ this.parentWatchComponent.childrenIDs.splice(index, 1);
408
+ }
409
+ }
410
+ this.parentWatchComponent = null;
411
+ }
412
+
413
+ // Set destroyed flag early to prevent operations
414
+ this.isDestroyed = true;
415
+
416
+ // Nullify all references for garbage collection
417
+ this.markup = null;
418
+ this.openTag = null;
419
+ this.closeTag = null;
420
+ this.renderBlock = null;
421
+ this.controller = null;
422
+ this.states = null;
423
+ this.App = null;
424
+ }
425
+
426
+ /**
427
+ * Mount child view controllers (using ChildrenRegistry)
428
+ * @private
429
+ */
430
+ _mountChildren() {
431
+ if (!this.controller?._childrenRegistry) return;
432
+
433
+ try {
434
+ const registry = this.controller._childrenRegistry;
435
+
436
+ // Get children belonging to this reactive component from registry
437
+ const rcChildren = registry.getReactiveComponentChildren(this.id);
438
+
439
+ // Mount all children belonging to this reactive component
440
+ rcChildren.forEach(childNode => {
441
+ const childId = childNode.scope.id;
442
+ if (!registry.isMounted(childId)) {
443
+ registry.mount(childId);
444
+ }
445
+ });
446
+
447
+ // Also mount children in childrenIDs (for backward compatibility)
448
+ if (this.childrenIDs.length > 0) {
449
+ this.childrenIDs.forEach(childId => {
450
+ if (!registry.isMounted(childId)) {
451
+ registry.mount(childId);
452
+ }
453
+ });
454
+ }
455
+ } catch (error) {
456
+ console.error(`ReactiveComponent mount children error [${this.type}][${this.id}]:`, error);
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Unmount child view controllers (using ChildrenRegistry)
462
+ * @private
463
+ */
464
+ _unmountChildren() {
465
+ if (!this.controller?._childrenRegistry) return;
466
+
467
+ try {
468
+ const registry = this.controller._childrenRegistry;
469
+
470
+ // Get children belonging to this reactive component from registry
471
+ const rcChildren = registry.getReactiveComponentChildren(this.id);
472
+
473
+ // Unmount all children belonging to this reactive component
474
+ rcChildren.forEach(childNode => {
475
+ const childId = childNode.scope.id;
476
+ if (registry.isMounted(childId)) {
477
+ registry.unmount(childId);
478
+ }
479
+ });
480
+
481
+ // Also unmount children in childrenIDs (for backward compatibility)
482
+ if (this.childrenIDs.length > 0) {
483
+ this.childrenIDs.forEach(childId => {
484
+ if (registry.isMounted(childId)) {
485
+ registry.unmount(childId);
486
+ }
487
+ });
488
+ }
489
+ } catch (error) {
490
+ console.error(`ReactiveComponent unmount children error [${this.type}][${this.id}]:`, error);
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Unsubscribe all state subscriptions
496
+ *
497
+ * v2.0.0 - Enhanced error handling:
498
+ * - Continues even if individual unsubscribe fails
499
+ * - Clears array after all attempts
500
+ * - Prevents partial cleanup
501
+ *
502
+ * @private
503
+ */
504
+ _unsubscribeAll() {
505
+ const errors = [];
506
+
507
+ // Try to unsubscribe all, collecting errors
508
+ this.subscribes.forEach((unsubscribe, index) => {
509
+ try {
510
+ if (typeof unsubscribe === 'function') {
511
+ unsubscribe();
512
+ }
513
+ } catch (error) {
514
+ errors.push({ index, error });
515
+ console.error(`ReactiveComponent unsubscribe error [${this.type}] at index ${index}:`, error);
516
+ }
517
+ });
518
+
519
+ // Always clear array, even if some failed
520
+ this.subscribes.length = 0;
521
+
522
+ // Log summary if errors occurred
523
+ if (errors.length > 0) {
524
+ console.warn(`ReactiveComponent [${this.type}][${this.id}]: ${errors.length} unsubscribe errors occurred`);
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Get current state values
530
+ * @returns {Object}
531
+ */
532
+ getStateValues() {
533
+ if (!this.stateKeys.length) return {};
534
+
535
+ return this.stateKeys.reduce((acc, key) => {
536
+ acc[key] = this.states[key];
537
+ return acc;
538
+ }, {});
539
+ }
540
+
541
+ /**
542
+ * Check if component has state dependencies
543
+ * @returns {boolean}
544
+ */
545
+ hasStateDependencies() {
546
+ return this.stateKeys.length > 0;
547
+ }
548
+
549
+ /**
550
+ * Force update without state change check
551
+ */
552
+ forceUpdate() {
553
+ const wasUpdating = this._isUpdating;
554
+ this._isUpdating = false;
555
+ this.update();
556
+ this._isUpdating = wasUpdating;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Factory function for creating reactive components
562
+ * Used by compiler: __reactive(reactiveID, stateKeys, renderBlock, options)
563
+ *
564
+ * @param {ViewController} controller - View controller
565
+ * @param {string} reactiveID - Component ID
566
+ * @param {Array<string>} stateKeys - State keys to watch
567
+ * @param {Function} renderBlock - Render function
568
+ * @param {Object} options - Component options
569
+ * @returns {ReactiveComponent}
570
+ */
571
+ export function __reactive(controller, reactiveID, stateKeys, renderBlock, options = {}) {
572
+ const {
573
+ type = 'output',
574
+ escapeHTML = false,
575
+ parentWatchComponent = null
576
+ } = options;
577
+
578
+ const component = new ReactiveComponent({
579
+ App: controller.App,
580
+ controller,
581
+ stateKeys,
582
+ renderBlock,
583
+ renderID: reactiveID,
584
+ type,
585
+ escapeHTML,
586
+ parentWatchComponent
587
+ });
588
+
589
+ return component;
590
+ }
591
+
592
+ export default ReactiveComponent;