neo.mjs 10.0.0-beta.3 → 10.0.0-beta.5

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 (76) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/portal/index.html +1 -1
  4. package/apps/portal/view/ViewportController.mjs +1 -1
  5. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  6. package/apps/portal/view/learn/MainContainerController.mjs +6 -6
  7. package/examples/button/effect/MainContainer.mjs +207 -0
  8. package/examples/button/effect/app.mjs +6 -0
  9. package/examples/button/effect/index.html +11 -0
  10. package/examples/button/effect/neo-config.json +6 -0
  11. package/learn/guides/{Collections.md → datahandling/Collections.md} +6 -6
  12. package/learn/guides/datahandling/Grids.md +621 -0
  13. package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
  14. package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
  15. package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
  16. package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
  17. package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
  18. package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
  19. package/learn/tree.json +64 -57
  20. package/package.json +3 -3
  21. package/src/DefaultConfig.mjs +2 -2
  22. package/src/Neo.mjs +244 -88
  23. package/src/button/Effect.mjs +435 -0
  24. package/src/collection/Base.mjs +35 -3
  25. package/src/component/Base.mjs +72 -61
  26. package/src/container/Base.mjs +28 -24
  27. package/src/controller/Base.mjs +87 -63
  28. package/src/core/Base.mjs +207 -33
  29. package/src/core/Compare.mjs +3 -13
  30. package/src/core/Config.mjs +230 -0
  31. package/src/core/ConfigSymbols.mjs +3 -0
  32. package/src/core/Effect.mjs +127 -0
  33. package/src/core/EffectBatchManager.mjs +68 -0
  34. package/src/core/EffectManager.mjs +38 -0
  35. package/src/core/Util.mjs +3 -18
  36. package/src/data/RecordFactory.mjs +22 -3
  37. package/src/grid/Container.mjs +8 -4
  38. package/src/grid/column/Component.mjs +1 -1
  39. package/src/state/Provider.mjs +343 -452
  40. package/src/state/createHierarchicalDataProxy.mjs +124 -0
  41. package/src/tab/header/EffectButton.mjs +75 -0
  42. package/src/util/Function.mjs +52 -5
  43. package/src/vdom/Helper.mjs +9 -10
  44. package/src/vdom/VNode.mjs +1 -1
  45. package/src/worker/App.mjs +0 -5
  46. package/test/siesta/siesta.js +32 -0
  47. package/test/siesta/tests/CollectionBase.mjs +10 -10
  48. package/test/siesta/tests/VdomHelper.mjs +22 -59
  49. package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
  50. package/test/siesta/tests/config/Basic.mjs +149 -0
  51. package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
  52. package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
  53. package/test/siesta/tests/config/Hierarchy.mjs +94 -0
  54. package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
  55. package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
  56. package/test/siesta/tests/core/Effect.mjs +131 -0
  57. package/test/siesta/tests/core/EffectBatching.mjs +322 -0
  58. package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
  59. package/test/siesta/tests/state/Provider.mjs +537 -0
  60. package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
  61. package/learn/guides/ExtendingNeoClasses.md +0 -331
  62. /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
  63. /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
  64. /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
  65. /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
  66. /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
  67. /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
  68. /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
  69. /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
  70. /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
  71. /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
  72. /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
  73. /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
  74. /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
  75. /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
  76. /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
package/src/Neo.mjs CHANGED
@@ -1,17 +1,46 @@
1
- import DefaultConfig from './DefaultConfig.mjs';
1
+ import DefaultConfig from './DefaultConfig.mjs';
2
+ import {isDescriptor} from './core/ConfigSymbols.mjs';
2
3
 
3
4
  const
4
5
  camelRegex = /-./g,
5
6
  configSymbol = Symbol.for('configSymbol'),
6
7
  getSetCache = Symbol('getSetCache'),
8
+ cloneMap = {
9
+ Array(obj, deep, ignoreNeoInstances) {
10
+ return !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))]
11
+ },
12
+ Date(obj) {
13
+ return new Date(obj.valueOf())
14
+ },
15
+ Map(obj) {
16
+ return new Map(obj) // shallow copy
17
+ },
18
+ NeoInstance(obj, ignoreNeoInstances) {
19
+ return ignoreNeoInstances ? obj : Neo.cloneNeoInstance(obj)
20
+ },
21
+ Set(obj) {
22
+ return new Set(obj)
23
+ },
24
+ Object(obj, deep, ignoreNeoInstances) {
25
+ const out = {};
26
+
27
+ // Use Reflect.ownKeys() to include symbol properties (e.g., for config descriptors)
28
+ Reflect.ownKeys(obj).forEach(key => {
29
+ const value = obj[key];
30
+ out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
31
+ });
32
+
33
+ return out
34
+ }
35
+ },
7
36
  typeDetector = {
8
37
  function: item => {
9
- if (item.prototype?.constructor.isClass) {
38
+ if (item.prototype?.constructor?.isClass) {
10
39
  return 'NeoClass'
11
40
  }
12
41
  },
13
42
  object: item => {
14
- if (item.constructor.isClass && item instanceof Neo.core.Base) {
43
+ if (item.constructor?.isClass && item instanceof Neo.core.Base) {
15
44
  return 'NeoInstance'
16
45
  }
17
46
  }
@@ -22,7 +51,6 @@ const
22
51
  * @module Neo
23
52
  * @singleton
24
53
  * @borrows Neo.core.Util.bindMethods as bindMethods
25
- * @borrows Neo.core.Util.capitalize as capitalize
26
54
  * @borrows Neo.core.Util.createStyleObject as createStyleObject
27
55
  * @borrows Neo.core.Util.createStyles as createStyles
28
56
  * @borrows Neo.core.Util.decamel as decamel
@@ -177,25 +205,7 @@ Neo = globalThis.Neo = Object.assign({
177
205
  * @returns {Object|Array|*} the cloned input
178
206
  */
179
207
  clone(obj, deep=false, ignoreNeoInstances=false) {
180
- let out;
181
-
182
- return {
183
- Array : () => !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))],
184
- Date : () => new Date(obj.valueOf()),
185
- Map : () => new Map(obj), // shallow copy
186
- NeoInstance: () => ignoreNeoInstances ? obj : this.cloneNeoInstance(obj),
187
- Set : () => new Set(obj),
188
-
189
- Object: () => {
190
- out = {};
191
-
192
- Object.entries(obj).forEach(([key, value]) => {
193
- out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
194
- });
195
-
196
- return out
197
- }
198
- }[Neo.typeOf(obj)]?.() || obj
208
+ return cloneMap[Neo.typeOf(obj)]?.(obj, deep, ignoreNeoInstances) || obj
199
209
  },
200
210
 
201
211
  /**
@@ -263,7 +273,7 @@ Neo = globalThis.Neo = Object.assign({
263
273
  return null
264
274
  }
265
275
 
266
- className = config.className || config.module.prototype.className;
276
+ className = config.className || config.module.prototype.className
267
277
  }
268
278
 
269
279
  if (!exists(className)) {
@@ -324,6 +334,10 @@ Neo = globalThis.Neo = Object.assign({
324
334
  return Neo.merge(Neo.merge(target, defaults), source)
325
335
  }
326
336
 
337
+ if (!target) {
338
+ return source
339
+ }
340
+
327
341
  for (const key in source) {
328
342
  const value = source[key];
329
343
 
@@ -337,6 +351,35 @@ Neo = globalThis.Neo = Object.assign({
337
351
  return target
338
352
  },
339
353
 
354
+ /**
355
+ * Merges a new value into an existing config value based on a specified strategy.
356
+ * This method is used during instance creation to apply merge strategies defined in config descriptors.
357
+ * @param {any} defaultValue - The default value of the config (from static config).
358
+ * @param {any} instanceValue - The value provided during instance creation.
359
+ * @param {string|Function} strategy - The merge strategy: 'shallow', 'deep', 'replace', or a custom function.
360
+ * @returns {any} The merged value.
361
+ */
362
+ mergeConfig(defaultValue, instanceValue, strategy) {
363
+ const
364
+ defaultValueType = Neo.typeOf(defaultValue),
365
+ instanceValueType = Neo.typeOf(instanceValue);
366
+
367
+ if (strategy === 'shallow') {
368
+ if (defaultValueType === 'Object' && instanceValueType === 'Object') {
369
+ return {...defaultValue, ...instanceValue}
370
+ }
371
+ } else if (strategy === 'deep') {
372
+ if (defaultValueType === 'Object' && instanceValueType === 'Object') {
373
+ return Neo.merge(Neo.clone(defaultValue, true), instanceValue)
374
+ }
375
+ } else if (typeof strategy === 'function') {
376
+ return strategy(defaultValue, instanceValue)
377
+ }
378
+
379
+ // Default to 'replace' or if strategy is not recognized
380
+ return instanceValue
381
+ },
382
+
340
383
  /**
341
384
  * Maps a className string into a given or global namespace
342
385
  * @example
@@ -460,13 +503,14 @@ Neo = globalThis.Neo = Object.assign({
460
503
  * @returns {T}
461
504
  */
462
505
  setupClass(cls) {
463
- let baseCfg = null,
464
- ntypeChain = [],
465
- {ntypeMap} = Neo,
466
- proto = cls.prototype || cls,
467
- ns = Neo.ns(proto.constructor.config.className, false),
468
- protos = [],
469
- cfg, config, ctor, hierarchyInfo, ntype;
506
+ let baseConfig = null,
507
+ baseConfigDescriptors = null,
508
+ ntypeChain = [],
509
+ {ntypeMap} = Neo,
510
+ proto = cls.prototype || cls,
511
+ ns = Neo.ns(proto.constructor.config.className, false),
512
+ protos = [],
513
+ cfg, config, configDescriptors, ctor, hierarchyInfo, ntype;
470
514
 
471
515
  /*
472
516
  * If the namespace already exists, directly return it.
@@ -480,12 +524,16 @@ Neo = globalThis.Neo = Object.assign({
480
524
  return ns
481
525
  }
482
526
 
527
+ // Traverse the prototype chain to collect inherited configs and descriptors
483
528
  while (proto.__proto__) {
484
529
  ctor = proto.constructor;
485
530
 
531
+ // If a class in the prototype chain has already had its config applied,
532
+ // we can use its pre-processed config and descriptors as a base.
486
533
  if (Object.hasOwn(ctor, 'classConfigApplied')) {
487
- baseCfg = Neo.clone(ctor.config, true);
488
- ntypeChain = [...ctor.ntypeChain];
534
+ baseConfig = Neo.clone(ctor.config, true);
535
+ baseConfigDescriptors = Neo.clone(ctor.configDescriptors, true);
536
+ ntypeChain = [...ctor.ntypeChain];
489
537
  break
490
538
  }
491
539
 
@@ -493,29 +541,43 @@ Neo = globalThis.Neo = Object.assign({
493
541
  proto = proto.__proto__
494
542
  }
495
543
 
496
- config = baseCfg || {};
544
+ // Initialize accumulated config and descriptors
545
+ config = baseConfig || {};
546
+ configDescriptors = baseConfigDescriptors || {};
497
547
 
548
+ // Process each class in the prototype chain (from top to bottom)
498
549
  protos.forEach(element => {
499
550
  let mixins;
500
551
 
501
552
  ctor = element.constructor;
502
-
503
- cfg = ctor.config || {};
553
+ cfg = ctor.config || {};
504
554
 
505
555
  if (Neo.overwrites) {
506
556
  ctor.applyOverwrites?.(cfg)
507
557
  }
508
558
 
559
+ // Process each config property defined in the current class's static config
509
560
  Object.entries(cfg).forEach(([key, value]) => {
510
- if (key.slice(-1) === '_') {
511
- delete cfg[key];
512
- key = key.slice(0, -1);
513
- cfg[key] = value;
514
- autoGenerateGetSet(element, key)
561
+ const
562
+ isReactive = key.slice(-1) === '_',
563
+ baseKey = isReactive ? key.slice(0, -1) : key;
564
+
565
+ // 1. Handle descriptors: If the value is a descriptor object, store it.
566
+ // The 'value' property of the descriptor is then used as the actual config value.
567
+ if (Neo.isObject(value) && value[isDescriptor] === true) {
568
+ ctor.configDescriptors ??= {};
569
+ ctor.configDescriptors[baseKey] = Neo.clone(value, true); // Deep clone to prevent mutation
570
+ value = value.value // Use the descriptor's value as the config value
515
571
  }
516
572
 
517
- // only apply properties which have no setters inside the prototype chain
518
- // those will get applied on create (Neo.core.Base -> initConfig)
573
+ // 2. Handle reactive vs. non-reactive configs: Generate getters/setters for reactive configs.
574
+ if (isReactive) {
575
+ delete cfg[key]; // Remove original key with underscore
576
+ cfg[baseKey] = value; // Use the potentially modified value
577
+ autoGenerateGetSet(element, baseKey)
578
+ }
579
+ // This part handles non-reactive configs (including those that were descriptors)
580
+ // If no property setter exists, define it directly on the prototype.
519
581
  else if (!Neo.hasPropertySetter(element, key)) {
520
582
  Object.defineProperty(element, key, {
521
583
  enumerable: true,
@@ -525,6 +587,17 @@ Neo = globalThis.Neo = Object.assign({
525
587
  }
526
588
  });
527
589
 
590
+ // Merge configDescriptors: Apply "first-defined wins" strategy.
591
+ // If a descriptor for a key already exists (from a parent class), it is not overwritten.
592
+ if (ctor.configDescriptors) {
593
+ for (const key in ctor.configDescriptors) {
594
+ if (!Object.hasOwn(configDescriptors, key)) {
595
+ configDescriptors[key] = Neo.clone(ctor.configDescriptors[key], true) // Deep clone for immutability
596
+ }
597
+ }
598
+ }
599
+
600
+ // Process ntype and ntypeChain
528
601
  if (Object.hasOwn(cfg, 'ntype')) {
529
602
  ntype = cfg.ntype;
530
603
 
@@ -539,6 +612,7 @@ Neo = globalThis.Neo = Object.assign({
539
612
  ntypeMap[ntype] = cfg.className
540
613
  }
541
614
 
615
+ // Process mixins
542
616
  mixins = Object.hasOwn(config, 'mixins') && config.mixins || [];
543
617
 
544
618
  if (ctor.observable) {
@@ -550,7 +624,7 @@ Neo = globalThis.Neo = Object.assign({
550
624
  }
551
625
 
552
626
  if (mixins.length > 0) {
553
- applyMixins(ctor, mixins);
627
+ applyMixins(ctor, mixins, cfg);
554
628
 
555
629
  if (Neo.ns('Neo.core.Observable', false, ctor.prototype.mixins)) {
556
630
  ctor.observable = true
@@ -560,29 +634,45 @@ Neo = globalThis.Neo = Object.assign({
560
634
  delete cfg.mixins;
561
635
  delete config.mixins;
562
636
 
563
- Object.assign(config, cfg);
637
+ // Hierarchical merging of static config values based on descriptors.
638
+ // This ensures that values are merged (e.g., shallow/deep) instead of simply overwritten.
639
+ Object.entries(cfg).forEach(([key, value]) => {
640
+ const descriptor = configDescriptors[key];
641
+
642
+ if (descriptor?.merge) {
643
+ config[key] = Neo.mergeConfig(config[key], value, descriptor.merge)
644
+ } else {
645
+ config[key] = value
646
+ }
647
+ });
564
648
 
649
+ // Assign final processed config and descriptors to the class constructor
565
650
  Object.assign(ctor, {
566
651
  classConfigApplied: true,
567
- config : Neo.clone(config, true),
652
+ config : Neo.clone(config, true), // Deep clone final config for immutability
653
+ configDescriptors : Neo.clone(configDescriptors, true), // Deep clone final descriptors for immutability
568
654
  isClass : true,
569
655
  ntypeChain
570
656
  });
571
657
 
658
+ // Apply to global namespace if not a singleton
572
659
  !config.singleton && this.applyToGlobalNs(cls)
573
660
  });
574
661
 
575
662
  proto = cls.prototype || cls;
576
663
 
664
+ // Add is<Ntype> flags to the prototype
577
665
  ntypeChain.forEach(ntype => {
578
666
  proto[`is${Neo.capitalize(Neo.camel(ntype))}`] = true
579
667
  });
580
668
 
669
+ // If it's a singleton, create and apply the instance to the global namespace
581
670
  if (proto.singleton) {
582
671
  cls = Neo.create(cls);
583
672
  Neo.applyToGlobalNs(cls)
584
673
  }
585
674
 
675
+ // Add class hierarchy information to the manager or a temporary map
586
676
  hierarchyInfo = {
587
677
  className : proto.className,
588
678
  module : cls,
@@ -609,7 +699,7 @@ Neo = globalThis.Neo = Object.assign({
609
699
  return null
610
700
  }
611
701
 
612
- return typeDetector[typeof item]?.(item) || item.constructor.name
702
+ return typeDetector[typeof item]?.(item) || item.constructor?.name
613
703
  }
614
704
  }, Neo);
615
705
 
@@ -623,6 +713,7 @@ const ignoreMixin = [
623
713
  'classConfigApplied',
624
714
  'className',
625
715
  'constructor',
716
+ 'id',
626
717
  'isClass',
627
718
  'mixin',
628
719
  'ntype',
@@ -635,9 +726,10 @@ const ignoreMixin = [
635
726
  /**
636
727
  * @param {Neo.core.Base} cls
637
728
  * @param {Array} mixins
729
+ * @param {Object} classConfig
638
730
  * @private
639
731
  */
640
- function applyMixins(cls, mixins) {
732
+ function applyMixins(cls, mixins, classConfig) {
641
733
  if (!Array.isArray(mixins)) {
642
734
  mixins = [mixins];
643
735
  }
@@ -659,12 +751,12 @@ function applyMixins(cls, mixins) {
659
751
  }
660
752
 
661
753
  mixinCls = Neo.ns(mixin);
662
- mixinProto = mixinCls.prototype;
754
+ mixinProto = mixinCls.prototype
663
755
  }
664
756
 
665
757
  mixinProto.className.split('.').reduce(mixReduce(mixinCls), mixinClasses);
666
758
 
667
- Object.getOwnPropertyNames(mixinProto).forEach(mixinProperty(cls.prototype, mixinProto))
759
+ Object.entries(Object.getOwnPropertyDescriptors(mixinProto)).forEach(mixinProperty(cls.prototype, mixinProto, classConfig))
668
760
  }
669
761
 
670
762
  cls.prototype.mixins = mixinClasses // todo: we should do a deep merge
@@ -682,30 +774,54 @@ function autoGenerateGetSet(proto, key) {
682
774
  throw('Config ' + key + '_ (' + proto.className + ') already has a set method, use beforeGet, beforeSet & afterSet instead')
683
775
  }
684
776
 
777
+ const
778
+ _key = '_' + key,
779
+ uKey = key[0].toUpperCase() + key.slice(1),
780
+ beforeGet = 'beforeGet' + uKey,
781
+ beforeSet = 'beforeSet' + uKey,
782
+ afterSet = 'afterSet' + uKey;
783
+
685
784
  if (!Neo[getSetCache]) {
686
785
  Neo[getSetCache] = {}
687
786
  }
688
787
 
689
788
  if (!Neo[getSetCache][key]) {
789
+ // Public Descriptor
690
790
  Neo[getSetCache][key] = {
691
791
  get() {
692
792
  let me = this,
693
- beforeGet = `beforeGet${key[0].toUpperCase() + key.slice(1)}`,
793
+ config = me.getConfig(key),
694
794
  hasNewKey = Object.hasOwn(me[configSymbol], key),
695
795
  newKey = me[configSymbol][key],
696
- value = hasNewKey ? newKey : me['_' + key];
796
+ value = hasNewKey ? newKey : me[_key];
697
797
 
698
- if (Array.isArray(value)) {
699
- if (key !== 'items') {
700
- value = [...value]
798
+ if (value instanceof Date) {
799
+ value = new Date(value.valueOf());
800
+ }
801
+ // new, explicit opt-in path
802
+ else if (config.cloneOnGet) {
803
+ const {cloneOnGet} = config;
804
+
805
+ if (cloneOnGet === 'deep') {
806
+ value = Neo.clone(value, true, true);
807
+ } else if (cloneOnGet === 'shallow') {
808
+ const type = Neo.typeOf(value);
809
+
810
+ if (type === 'Array') {
811
+ value = [...value];
812
+ } else if (type === 'Object') {
813
+ value = {...value};
814
+ }
701
815
  }
702
- } else if (value instanceof Date) {
703
- value = new Date(value.valueOf())
816
+ }
817
+ // legacy behavior
818
+ else if (Array.isArray(value)) {
819
+ value = [...value];
704
820
  }
705
821
 
706
822
  if (hasNewKey) {
707
- me[key] = value; // we do want to trigger the setter => beforeSet, afterSet
708
- value = me['_' + key]; // return the value parsed by the setter
823
+ me[key] = value; // We do want to trigger the setter => beforeSet, afterSet
824
+ value = me[_key]; // Return the value parsed by the setter
709
825
  delete me[configSymbol][key]
710
826
  }
711
827
 
@@ -715,28 +831,37 @@ function autoGenerateGetSet(proto, key) {
715
831
 
716
832
  return value
717
833
  },
718
-
719
834
  set(value) {
720
- if (value === undefined) {
721
- return
722
- }
835
+ if (value === undefined) return;
723
836
 
724
- let me = this,
725
- _key = '_' + key,
726
- uKey = key[0].toUpperCase() + key.slice(1),
727
- beforeSet = 'beforeSet' + uKey,
728
- afterSet = 'afterSet' + uKey,
729
- oldValue = me[_key];
837
+ const config = this.getConfig(key);
838
+ if (!config) return;
839
+
840
+ let me = this,
841
+ oldValue = config.get(), // Get the old value from the Config instance
842
+ {EffectBatchManager} = Neo.core,
843
+ isNewBatch = !EffectBatchManager?.isBatchActive();
730
844
 
731
- // every set call has to delete the matching symbol
845
+ // If a config change is not triggered via `core.Base#set()`, honor changes inside hooks.
846
+ isNewBatch && EffectBatchManager?.startBatch();
847
+
848
+ // 1. Prevent infinite loops:
849
+ // Immediately remove the pending value from the configSymbol to prevent a getter from
850
+ // recursively re-triggering this setter.
732
851
  delete me[configSymbol][key];
733
852
 
734
- if (key !== 'items' && key !== 'vnode') {
735
- value = Neo.clone(value, true, true)
853
+ switch (config.clone) {
854
+ case 'deep':
855
+ value = Neo.clone(value, true, true);
856
+ break;
857
+ case 'shallow':
858
+ value = Neo.clone(value, false, true);
859
+ break;
736
860
  }
737
861
 
738
- // we do want to store the value before the beforeSet modification as well,
739
- // since it could get pulled by other beforeSet methods of different configs
862
+ // 2. Create a temporary state for beforeSet hooks:
863
+ // Set the new value directly on the private backing property. This allows any beforeSet
864
+ // hook to access the new value of this and other configs within the same `set()` call.
740
865
  me[_key] = value;
741
866
 
742
867
  if (typeof me[beforeSet] === 'function') {
@@ -744,25 +869,43 @@ function autoGenerateGetSet(proto, key) {
744
869
 
745
870
  // If they don't return a value, that means no change
746
871
  if (value === undefined) {
872
+ // Restore the original value if the update is canceled.
747
873
  me[_key] = oldValue;
874
+ isNewBatch && EffectBatchManager?.endBatch();
748
875
  return
749
876
  }
750
-
751
- me[_key] = value;
752
877
  }
753
878
 
754
- if (
755
- (key === 'vnode' && value !== oldValue) || // vnode trees can be huge, avoid a deep comparison
756
- !Neo.isEqual(value, oldValue)
757
- ) {
879
+ // 3. Restore state for change detection:
880
+ // Revert the private backing property to its original value. This is crucial for the
881
+ // `config.set()` method to correctly detect if the value has actually changed.
882
+ me[_key] = oldValue;
883
+
884
+ // 4. Finalize the change:
885
+ // The config.set() method performs the final check and, if the value changed,
886
+ // triggers afterSet hooks and notifies subscribers.
887
+ if (config.set(value)) {
758
888
  me[afterSet]?.(value, oldValue);
759
889
  me.afterSetConfig?.(key, value, oldValue)
760
890
  }
891
+
892
+ isNewBatch && EffectBatchManager?.endBatch()
893
+ }
894
+ };
895
+
896
+ // Private Descriptor
897
+ Neo[getSetCache][_key] = {
898
+ get() {
899
+ return this.getConfig(key)?.get()
900
+ },
901
+ set(value) {
902
+ this.getConfig(key)?.setRaw(value)
761
903
  }
762
904
  }
763
905
  }
764
906
 
765
- Object.defineProperty(proto, key, Neo[getSetCache][key])
907
+ Object.defineProperty(proto, key, Neo[getSetCache][key]);
908
+ Object.defineProperty(proto, _key, Neo[getSetCache][_key])
766
909
  }
767
910
 
768
911
  /**
@@ -783,9 +926,7 @@ function createArrayNs(create, current, prev) {
783
926
  arrRoot = prev[arrDetails[0]]
784
927
  }
785
928
 
786
- if (!arrRoot) {
787
- return
788
- }
929
+ if (!arrRoot) return;
789
930
 
790
931
  for (; i < len; i++) {
791
932
  arrItem = parseInt(arrDetails[i]);
@@ -819,12 +960,27 @@ function exists(className) {
819
960
  /**
820
961
  * @param {Neo.core.Base} proto
821
962
  * @param {Neo.core.Base} mixinProto
963
+ * @param {Object} classConfig
822
964
  * @returns {Function}
823
965
  * @private
824
966
  */
825
- function mixinProperty(proto, mixinProto) {
826
- return function(key) {
827
- if (~ignoreMixin.indexOf(key)) {
967
+ function mixinProperty(proto, mixinProto, classConfig) {
968
+ return function([key, descriptor]) {
969
+ if (ignoreMixin.includes(key)) return;
970
+
971
+ // Mixins must not override existing class properties with a setter
972
+ if (Neo.hasPropertySetter(proto, key)) return;
973
+
974
+ // Reactive neo configs, or public class fields defined via get() AND set()
975
+ if (descriptor.get && descriptor.set) {
976
+ autoGenerateGetSet(proto, key);
977
+
978
+ const mixinClassConfig = mixinProto.constructor.config;
979
+
980
+ if (Object.hasOwn(mixinClassConfig, key)) {
981
+ classConfig[key] = mixinClassConfig[key];
982
+ }
983
+
828
984
  return
829
985
  }
830
986
 
@@ -871,7 +1027,7 @@ function parseArrayFromString(str) {
871
1027
  )
872
1028
  }
873
1029
 
874
- Neo.config = Neo.config || {};
1030
+ Neo.config ??= {};
875
1031
 
876
1032
  Neo.assignDefaults(Neo.config, DefaultConfig);
877
1033