neo.mjs 10.0.0-beta.4 → 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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +2 -2
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/button/effect/MainContainer.mjs +207 -0
- package/examples/button/effect/app.mjs +6 -0
- package/examples/button/effect/index.html +11 -0
- package/examples/button/effect/neo-config.json +6 -0
- package/learn/guides/datahandling/StateProviders.md +1 -0
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/tree.json +1 -0
- package/package.json +2 -2
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +226 -78
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +7 -2
- package/src/component/Base.mjs +67 -46
- package/src/container/Base.mjs +28 -24
- package/src/core/Base.mjs +138 -19
- package/src/core/Config.mjs +123 -32
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/grid/Container.mjs +8 -4
- package/src/grid/column/Component.mjs +1 -1
- package/src/state/Provider.mjs +343 -452
- package/src/state/createHierarchicalDataProxy.mjs +124 -0
- package/src/tab/header/EffectButton.mjs +75 -0
- package/src/vdom/Helper.mjs +9 -10
- package/src/vdom/VNode.mjs +1 -1
- package/src/worker/App.mjs +0 -5
- package/test/siesta/siesta.js +32 -0
- package/test/siesta/tests/CollectionBase.mjs +10 -10
- package/test/siesta/tests/VdomHelper.mjs +22 -59
- package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
- package/test/siesta/tests/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
- package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
- package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
- package/test/siesta/tests/config/Hierarchy.mjs +94 -0
- package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
- package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
- package/test/siesta/tests/core/Effect.mjs +131 -0
- package/test/siesta/tests/core/EffectBatching.mjs +322 -0
- package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
- package/test/siesta/tests/state/Provider.mjs +537 -0
- package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
package/src/container/Base.mjs
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
import Component
|
2
|
-
import LayoutBase
|
3
|
-
import LayoutCard
|
4
|
-
import LayoutFit
|
5
|
-
import LayoutGrid
|
6
|
-
import LayoutHbox
|
7
|
-
import LayoutVBox
|
8
|
-
import Logger
|
9
|
-
import NeoArray
|
1
|
+
import Component from '../component/Base.mjs';
|
2
|
+
import LayoutBase from '../layout/Base.mjs';
|
3
|
+
import LayoutCard from '../layout/Card.mjs';
|
4
|
+
import LayoutFit from '../layout/Fit.mjs';
|
5
|
+
import LayoutGrid from '../layout/Grid.mjs';
|
6
|
+
import LayoutHbox from '../layout/HBox.mjs';
|
7
|
+
import LayoutVBox from '../layout/VBox.mjs';
|
8
|
+
import Logger from '../util/Logger.mjs';
|
9
|
+
import NeoArray from '../util/Array.mjs';
|
10
|
+
import {isDescriptor} from '../core/ConfigSymbols.mjs';
|
10
11
|
|
11
12
|
const byWeight = ({ weight : lhs = 0 }, { weight : rhs = 0 }) => lhs - rhs;
|
12
13
|
|
@@ -31,9 +32,15 @@ class Container extends Component {
|
|
31
32
|
*/
|
32
33
|
baseCls: ['neo-container'],
|
33
34
|
/**
|
34
|
-
*
|
35
|
+
* Default configuration for child items within this container.
|
36
|
+
* This config uses a descriptor to enable deep merging with instance based itemDefaults.
|
37
|
+
* @member {Object} itemDefaults_={[isDescriptor]: true, merge: 'deep', value: null}
|
35
38
|
*/
|
36
|
-
itemDefaults_:
|
39
|
+
itemDefaults_: {
|
40
|
+
[isDescriptor]: true,
|
41
|
+
merge : 'deep',
|
42
|
+
value : null
|
43
|
+
},
|
37
44
|
/**
|
38
45
|
* An array or an object of config objects|instances|modules for each child component
|
39
46
|
* @member {Object[]} items_=[]
|
@@ -85,7 +92,13 @@ class Container extends Component {
|
|
85
92
|
* ]
|
86
93
|
* });
|
87
94
|
*/
|
88
|
-
items_:
|
95
|
+
items_: {
|
96
|
+
[isDescriptor]: true,
|
97
|
+
clone : 'shallow',
|
98
|
+
cloneOnGet : 'none',
|
99
|
+
isEqual : () => false,
|
100
|
+
value : []
|
101
|
+
},
|
89
102
|
/**
|
90
103
|
* It is crucial to define a layout before the container does get rendered.
|
91
104
|
* Meaning: onConstructed() is the latest life-cycle point.
|
@@ -342,10 +355,7 @@ class Container extends Component {
|
|
342
355
|
}
|
343
356
|
|
344
357
|
item.set(config);
|
345
|
-
|
346
|
-
// In case an item got created outside a stateProvider based hierarchy, there might be bindings or string
|
347
|
-
// based listeners which still need to get resolved.
|
348
|
-
item.getStateProvider()?.parseConfig(item);
|
358
|
+
item.getStateProvider()?.createBindings(item);
|
349
359
|
break
|
350
360
|
}
|
351
361
|
|
@@ -617,14 +627,8 @@ class Container extends Component {
|
|
617
627
|
config = super.mergeConfig(...args),
|
618
628
|
ctorItems;
|
619
629
|
|
620
|
-
//
|
621
|
-
//
|
622
|
-
|
623
|
-
if (config.itemDefaults) {
|
624
|
-
me._itemDefaults = Neo.clone(config.itemDefaults, true, true);
|
625
|
-
delete config.itemDefaults
|
626
|
-
}
|
627
|
-
|
630
|
+
// Avoid any interference on prototype level
|
631
|
+
// Does not clone existing Neo instances
|
628
632
|
if (config.items) {
|
629
633
|
ctorItems = me.constructor.config.items;
|
630
634
|
|
package/src/core/Base.mjs
CHANGED
@@ -3,7 +3,8 @@ import Compare from '../core/Co
|
|
3
3
|
import Util from '../core/Util.mjs';
|
4
4
|
import Config from './Config.mjs';
|
5
5
|
import {isDescriptor} from './ConfigSymbols.mjs';
|
6
|
-
import IdGenerator from './IdGenerator.mjs'
|
6
|
+
import IdGenerator from './IdGenerator.mjs';
|
7
|
+
import EffectBatchManager from './EffectBatchManager.mjs';
|
7
8
|
|
8
9
|
const configSymbol = Symbol.for('configSymbol'),
|
9
10
|
forceAssignConfigs = Symbol('forceAssignConfigs'),
|
@@ -98,12 +99,19 @@ class Base {
|
|
98
99
|
* @protected
|
99
100
|
*/
|
100
101
|
isConstructed: false,
|
102
|
+
/**
|
103
|
+
* This config will be set to `true` as the very first action within the `destroy()` method.
|
104
|
+
* Effects can observe this config to clean themselves up.
|
105
|
+
* @member {Boolean} isDestroying_=false
|
106
|
+
* @protected
|
107
|
+
*/
|
108
|
+
isDestroying_: false,
|
101
109
|
/**
|
102
110
|
* The config will get set to `true` once the Promise of `async initAsync()` is resolved.
|
103
111
|
* You can use `afterSetIsReady()` to get notified once the ready state is reached.
|
104
112
|
* Since not all classes use the Observable mixin, Neo will not fire an event.
|
105
113
|
* method body.
|
106
|
-
* @member {Boolean}
|
114
|
+
* @member {Boolean} isReady_=false
|
107
115
|
*/
|
108
116
|
isReady_: false,
|
109
117
|
/**
|
@@ -135,6 +143,12 @@ class Base {
|
|
135
143
|
* @private
|
136
144
|
*/
|
137
145
|
#configs = {};
|
146
|
+
/**
|
147
|
+
* Internal cache for all config subscription cleanup functions.
|
148
|
+
* @member {Function[]} #configSubscriptionCleanups=[]
|
149
|
+
* @private
|
150
|
+
*/
|
151
|
+
#configSubscriptionCleanups = []
|
138
152
|
/**
|
139
153
|
* Internal cache for all timeout ids when using this.timeout()
|
140
154
|
* @member {Number[]} timeoutIds=[]
|
@@ -239,7 +253,7 @@ class Base {
|
|
239
253
|
if (oldValue) {
|
240
254
|
if (hasManager) {
|
241
255
|
Neo.manager.Instance.unregister(oldValue)
|
242
|
-
} else {
|
256
|
+
} else if (Neo.idMap) {
|
243
257
|
delete Neo.idMap[oldValue]
|
244
258
|
}
|
245
259
|
}
|
@@ -249,7 +263,7 @@ class Base {
|
|
249
263
|
Neo.manager.Instance.register(me);
|
250
264
|
} else {
|
251
265
|
Neo.idMap ??= {};
|
252
|
-
Neo.idMap[
|
266
|
+
Neo.idMap[value] = me
|
253
267
|
}
|
254
268
|
}
|
255
269
|
}
|
@@ -386,10 +400,16 @@ class Base {
|
|
386
400
|
destroy() {
|
387
401
|
let me = this;
|
388
402
|
|
403
|
+
me.isDestroying = true;
|
404
|
+
|
389
405
|
me.#timeoutIds.forEach(id => {
|
390
406
|
clearTimeout(id)
|
391
407
|
});
|
392
408
|
|
409
|
+
me.#configSubscriptionCleanups.forEach(cleanup => {
|
410
|
+
cleanup()
|
411
|
+
});
|
412
|
+
|
393
413
|
if (Base.instanceManagerAvailable === true) {
|
394
414
|
Neo.manager.Instance.unregister(me)
|
395
415
|
} else if (Neo.idMap) {
|
@@ -423,7 +443,7 @@ class Base {
|
|
423
443
|
let me = this;
|
424
444
|
|
425
445
|
if (!me.#configs[key] && me.isConfig(key)) {
|
426
|
-
me.#configs[key] = new Config()
|
446
|
+
me.#configs[key] = new Config(me.constructor.configDescriptors?.[key])
|
427
447
|
}
|
428
448
|
|
429
449
|
return me.#configs[key]
|
@@ -488,7 +508,7 @@ class Base {
|
|
488
508
|
Object.assign(me[configSymbol], me.mergeConfig(config, preventOriginalConfig));
|
489
509
|
delete me[configSymbol].id;
|
490
510
|
me.processConfigs();
|
491
|
-
me.isConfiguring = false
|
511
|
+
me.isConfiguring = false
|
492
512
|
}
|
493
513
|
|
494
514
|
/**
|
@@ -524,10 +544,12 @@ class Base {
|
|
524
544
|
* @returns {Boolean}
|
525
545
|
*/
|
526
546
|
isConfig(key) {
|
527
|
-
|
547
|
+
let me = this;
|
548
|
+
// If a `core.Config` controller is already created, return true (fastest possible check).
|
549
|
+
// If not, a config is considered "reactive" if it has a generated property setter
|
528
550
|
// AND it is present as a defined config in the merged static config hierarchy.
|
529
551
|
// Neo.setupClass() removes the underscore from the static config keys.
|
530
|
-
return Neo.hasPropertySetter(
|
552
|
+
return me.#configs[key] || (Neo.hasPropertySetter(me, key) && (key in me.constructor.config))
|
531
553
|
}
|
532
554
|
|
533
555
|
/**
|
@@ -539,7 +561,8 @@ class Base {
|
|
539
561
|
*/
|
540
562
|
mergeConfig(config, preventOriginalConfig) {
|
541
563
|
let me = this,
|
542
|
-
ctor = me.constructor
|
564
|
+
ctor = me.constructor,
|
565
|
+
configDescriptors, staticConfig;
|
543
566
|
|
544
567
|
if (!ctor.config) {
|
545
568
|
throw new Error('Neo.applyClassConfig has not been run on ' + me.className)
|
@@ -549,7 +572,70 @@ class Base {
|
|
549
572
|
me.originalConfig = Neo.clone(config, true, true)
|
550
573
|
}
|
551
574
|
|
552
|
-
|
575
|
+
configDescriptors = ctor.configDescriptors;
|
576
|
+
staticConfig = ctor.config;
|
577
|
+
|
578
|
+
if (configDescriptors) {
|
579
|
+
Object.entries(config).forEach(([key, instanceValue]) => {
|
580
|
+
const descriptor = configDescriptors[key];
|
581
|
+
|
582
|
+
if (descriptor?.merge) {
|
583
|
+
config[key] = Neo.mergeConfig(staticConfig[key], instanceValue, descriptor.merge)
|
584
|
+
}
|
585
|
+
})
|
586
|
+
}
|
587
|
+
|
588
|
+
return {...staticConfig, ...config}
|
589
|
+
}
|
590
|
+
|
591
|
+
/**
|
592
|
+
* Subscribes *this* instance (the subscriber) to changes of a specific config property on another instance (the publisher).
|
593
|
+
* Ensures automatic cleanup when *this* instance (the subscriber) is destroyed.
|
594
|
+
*
|
595
|
+
* @param {String|Neo.core.Base} publisher - The ID of the publisher instance or the instance reference itself.
|
596
|
+
* @param {String} configName - The name of the config property on the publisher to subscribe to (e.g., 'myConfig').
|
597
|
+
* @param {Function} fn - The callback function to execute when the config changes.
|
598
|
+
* @returns {Function} A cleanup function to manually unsubscribe if needed before this instance's destruction.
|
599
|
+
*
|
600
|
+
* @example
|
601
|
+
* // Subscribing to a config on another instance
|
602
|
+
* this.observeConfig(someOtherInstance, 'myConfig', (newValue, oldValue) => {
|
603
|
+
* console.log('myConfig changed:', newValue);
|
604
|
+
* });
|
605
|
+
*
|
606
|
+
* // Discouraged: Self-observation. Use afterSet<ConfigName>() hooks instead.
|
607
|
+
* this.observeConfig(this, 'myOwnConfig', (newValue, oldValue) => {
|
608
|
+
* console.log('myOwnConfig changed:', newValue);
|
609
|
+
* });
|
610
|
+
*/
|
611
|
+
observeConfig(publisher, configName, fn) {
|
612
|
+
let publisherInstance = publisher;
|
613
|
+
|
614
|
+
if (Neo.isString(publisher)) {
|
615
|
+
publisherInstance = Neo.get(publisher);
|
616
|
+
if (!publisherInstance) {
|
617
|
+
console.warn(`Publisher instance with ID '${publisher}' not found. Cannot subscribe.`);
|
618
|
+
return Neo.emptyFn
|
619
|
+
}
|
620
|
+
}
|
621
|
+
|
622
|
+
if (!(publisherInstance instanceof Neo.core.Base)) {
|
623
|
+
console.warn(`Invalid publisher provided. Must be a Neo.core.Base instance or its ID.`);
|
624
|
+
return Neo.emptyFn
|
625
|
+
}
|
626
|
+
|
627
|
+
const configController = publisherInstance.getConfig(configName);
|
628
|
+
|
629
|
+
if (!configController) {
|
630
|
+
console.warn(`Config '${configName}' not found on publisher instance ${publisherInstance.id}. Cannot subscribe.`);
|
631
|
+
return Neo.emptyFn
|
632
|
+
}
|
633
|
+
|
634
|
+
const cleanup = configController.subscribe({id: this.id, fn});
|
635
|
+
|
636
|
+
this.#configSubscriptionCleanups.push(cleanup);
|
637
|
+
|
638
|
+
return cleanup
|
553
639
|
}
|
554
640
|
|
555
641
|
/**
|
@@ -670,24 +756,58 @@ class Base {
|
|
670
756
|
}
|
671
757
|
|
672
758
|
/**
|
673
|
-
*
|
759
|
+
* set() accepts the following input as keys:
|
760
|
+
* 1. Non-reactive configs
|
761
|
+
* 2. Reactive configs
|
762
|
+
* 3. Class fields defined via value
|
763
|
+
* 4. Class fields defined via get() & set()
|
764
|
+
* 5. "Anything else" will get directly get assigned to the instance
|
765
|
+
*
|
766
|
+
* The logic resolves circular dependencies as good as possible and ensures that config related hooks:
|
767
|
+
* - beforeGet<Config>
|
768
|
+
* - beforeSet<Config>
|
769
|
+
* - afterSet<Config>
|
770
|
+
* can access all new values from the batch operation.
|
674
771
|
* @param {Object} values={}
|
675
772
|
*/
|
676
773
|
set(values={}) {
|
677
|
-
let me
|
774
|
+
let me = this,
|
775
|
+
classFieldsViaSet = {};
|
776
|
+
|
777
|
+
// Start batching for effects
|
778
|
+
EffectBatchManager.startBatch();
|
678
779
|
|
679
780
|
values = me.setFields(values);
|
680
781
|
|
681
782
|
// If the initial config processing is still running,
|
682
783
|
// finish this one first before dropping new values into the configSymbol.
|
683
|
-
//
|
784
|
+
// See: https://github.com/neomjs/neo/issues/2201
|
684
785
|
if (me[forceAssignConfigs] !== true && Object.keys(me[configSymbol]).length > 0) {
|
685
786
|
me.processConfigs()
|
686
787
|
}
|
687
788
|
|
789
|
+
// Store class fields which are defined via get() & set() and ensure they won't get added to the config symbol.
|
790
|
+
Object.entries(values).forEach(([key, value]) => {
|
791
|
+
if (!me.isConfig(key)) {
|
792
|
+
classFieldsViaSet[key] = value;
|
793
|
+
delete values[key]
|
794
|
+
}
|
795
|
+
})
|
796
|
+
|
797
|
+
// Add reactive configs to the configSymbol
|
688
798
|
Object.assign(me[configSymbol], values);
|
689
799
|
|
690
|
-
|
800
|
+
// Process class fields which are defined via get() & set() => now they can access the latest values
|
801
|
+
// for reactive and non-reactive configs, as well as class fields defined with values.
|
802
|
+
Object.entries(classFieldsViaSet).forEach(([key, value]) => {
|
803
|
+
me[key] = value
|
804
|
+
})
|
805
|
+
|
806
|
+
// Process reactive configs
|
807
|
+
me.processConfigs(true);
|
808
|
+
|
809
|
+
// End batching for effects
|
810
|
+
EffectBatchManager.endBatch();
|
691
811
|
}
|
692
812
|
|
693
813
|
/**
|
@@ -698,15 +818,14 @@ class Base {
|
|
698
818
|
* @protected
|
699
819
|
*/
|
700
820
|
setFields(config) {
|
701
|
-
let me
|
702
|
-
configNames = me.constructor.config;
|
821
|
+
let me = this;
|
703
822
|
|
704
823
|
Object.entries(config).forEach(([key, value]) => {
|
705
|
-
if (!
|
824
|
+
if (!me.isConfig(key) && !Neo.hasPropertySetter(me, key)) {
|
706
825
|
me[key] = value;
|
707
826
|
delete config[key]
|
708
827
|
}
|
709
|
-
})
|
828
|
+
});
|
710
829
|
|
711
830
|
return config
|
712
831
|
}
|
@@ -748,7 +867,7 @@ class Base {
|
|
748
867
|
* @returns {String}
|
749
868
|
*/
|
750
869
|
get [Symbol.toStringTag]() {
|
751
|
-
return
|
870
|
+
return this.className
|
752
871
|
}
|
753
872
|
|
754
873
|
/**
|
package/src/core/Config.mjs
CHANGED
@@ -1,45 +1,52 @@
|
|
1
|
+
import EffectManager from './EffectManager.mjs';
|
1
2
|
import {isDescriptor} from './ConfigSymbols.mjs';
|
2
3
|
|
3
4
|
/**
|
4
|
-
* @src/util/ClassSystem.mjs Neo.core.Config
|
5
|
-
* @private
|
6
|
-
* @internal
|
7
|
-
*
|
8
5
|
* Represents an observable container for a config property.
|
9
6
|
* This class manages the value of a config, its subscribers, and custom behaviors
|
10
7
|
* like merge strategies and equality checks defined via a descriptor object.
|
11
8
|
*
|
12
9
|
* The primary purpose of this class is to enable fine-grained reactivity and
|
13
10
|
* decoupled cross-instance state sharing within the Neo.mjs framework.
|
11
|
+
* @class Neo.core.Config
|
12
|
+
* @private
|
13
|
+
* @internal
|
14
14
|
*/
|
15
15
|
class Config {
|
16
16
|
/**
|
17
|
-
*
|
17
|
+
* A Set to store callback functions that subscribe to changes in this config's value.
|
18
18
|
* @private
|
19
|
-
* @apps/portal/view/about/MemberContainer.mjs {any} #value
|
20
19
|
*/
|
21
|
-
#
|
22
|
-
|
20
|
+
#subscribers = {}
|
23
21
|
/**
|
24
|
-
*
|
22
|
+
* The internal value of the config property.
|
23
|
+
* @member #value
|
25
24
|
* @private
|
26
|
-
* @apps/portal/view/about/MemberContainer.mjs {Set<Function>} #subscribers
|
27
25
|
*/
|
28
|
-
#
|
26
|
+
#value
|
27
|
+
/**
|
28
|
+
* The cloning strategy to use when setting a new value.
|
29
|
+
* Supported values: 'deep', 'shallow', 'none'.
|
30
|
+
* @member {String} clone='deep'
|
31
|
+
*/
|
29
32
|
|
30
33
|
/**
|
31
|
-
* The strategy to use when
|
32
|
-
*
|
33
|
-
* @
|
34
|
+
* The cloning strategy to use when getting a value.
|
35
|
+
* Supported values: 'deep', 'shallow', 'none'.
|
36
|
+
* @member {String} cloneOnGet=null
|
34
37
|
*/
|
35
|
-
mergeStrategy = 'deep';
|
36
38
|
|
37
39
|
/**
|
38
40
|
* The function used to compare new and old values for equality.
|
39
41
|
* Defaults to `Neo.isEqual`. Can be overridden via a descriptor.
|
40
|
-
* @
|
42
|
+
* @member {Function} isEqual=Neo.isEqual
|
43
|
+
*/
|
44
|
+
|
45
|
+
/**
|
46
|
+
* The strategy to use when merging new values into this config.
|
47
|
+
* Defaults to 'replace'. Can be overridden via a descriptor merge property.
|
48
|
+
* @member {Function|String} mergeStrategy='replace'
|
41
49
|
*/
|
42
|
-
isEqual = Neo.isEqual;
|
43
50
|
|
44
51
|
/**
|
45
52
|
* Creates an instance of Config.
|
@@ -58,24 +65,49 @@ class Config {
|
|
58
65
|
* @returns {any} The current value.
|
59
66
|
*/
|
60
67
|
get() {
|
61
|
-
|
68
|
+
// Registers this Config instance as a dependency with the currently active Effect,
|
69
|
+
// enabling automatic re-execution when this Config's value changes.
|
70
|
+
EffectManager.getActiveEffect()?.addDependency(this);
|
71
|
+
return this.#value
|
62
72
|
}
|
63
73
|
|
64
74
|
/**
|
65
75
|
* Initializes the `Config` instance using a descriptor object.
|
66
|
-
* Extracts `mergeStrategy` and `isEqual` from the descriptor.
|
76
|
+
* Extracts `clone`, `mergeStrategy` and `isEqual` from the descriptor.
|
67
77
|
* The internal `#value` is NOT set by this method.
|
68
|
-
* @param {Object}
|
69
|
-
* @param {any}
|
70
|
-
* @param {string}
|
78
|
+
* @param {Object} descriptor - The descriptor object for the config.
|
79
|
+
* @param {any} descriptor.value - The default value for the config (not set by this method).
|
80
|
+
* @param {string} [descriptor.clone='deep'] - The clone strategy for set.
|
81
|
+
* @param {string} [descriptor.cloneOnGet] - The clone strategy for get. Defaults to 'shallow' if clone is 'deep' or 'shallow', and 'none' if clone is 'none'.
|
82
|
+
* @param {string} [descriptor.merge='deep'] - The merge strategy.
|
71
83
|
* @param {Function} [descriptor.isEqual=Neo.isEqual] - The equality comparison function.
|
72
84
|
*/
|
73
|
-
initDescriptor({isEqual, merge
|
85
|
+
initDescriptor({clone, cloneOnGet, isEqual, merge}) {
|
74
86
|
let me = this;
|
75
87
|
|
76
|
-
me
|
77
|
-
|
78
|
-
|
88
|
+
if (clone && clone !== me.clone) {
|
89
|
+
Object.defineProperty(me, 'clone', {
|
90
|
+
value: clone, writable: true, configurable: true, enumerable: true
|
91
|
+
})
|
92
|
+
}
|
93
|
+
|
94
|
+
if (cloneOnGet && cloneOnGet !== me.cloneOnGet) {
|
95
|
+
Object.defineProperty(me, 'cloneOnGet', {
|
96
|
+
value: cloneOnGet, writable: true, configurable: true, enumerable: true
|
97
|
+
})
|
98
|
+
}
|
99
|
+
|
100
|
+
if (isEqual && isEqual !== me.isEqual) {
|
101
|
+
Object.defineProperty(me, 'isEqual', {
|
102
|
+
value: isEqual, writable: true, configurable: true, enumerable: true
|
103
|
+
})
|
104
|
+
}
|
105
|
+
|
106
|
+
if (merge && merge !== me.mergeStrategy) {
|
107
|
+
Object.defineProperty(me, 'mergeStrategy', {
|
108
|
+
value: merge, writable: true, configurable: true, enumerable: true
|
109
|
+
})
|
110
|
+
}
|
79
111
|
}
|
80
112
|
|
81
113
|
/**
|
@@ -84,8 +116,13 @@ class Config {
|
|
84
116
|
* @param {any} oldValue - The old value of the config.
|
85
117
|
*/
|
86
118
|
notify(newValue, oldValue) {
|
87
|
-
for (const
|
88
|
-
|
119
|
+
for (const id in this.#subscribers) {
|
120
|
+
if (this.#subscribers.hasOwnProperty(id)) {
|
121
|
+
const subscriberSet = this.#subscribers[id];
|
122
|
+
for (const callback of subscriberSet) {
|
123
|
+
callback(newValue, oldValue)
|
124
|
+
}
|
125
|
+
}
|
89
126
|
}
|
90
127
|
}
|
91
128
|
|
@@ -127,13 +164,67 @@ class Config {
|
|
127
164
|
/**
|
128
165
|
* Subscribes a callback function to changes in this config's value.
|
129
166
|
* The callback will be invoked with `(newValue, oldValue)` whenever the config changes.
|
130
|
-
* @param {
|
167
|
+
* @param {Object} options - An object containing the subscription details.
|
168
|
+
* @param {String} options.id - The ID of the subscription owner (e.g., a Neo.core.Base instance's id).
|
169
|
+
* @param {Function} options.fn - The callback function.
|
131
170
|
* @returns {Function} A cleanup function to unsubscribe the callback.
|
132
171
|
*/
|
133
|
-
subscribe(
|
134
|
-
|
135
|
-
|
172
|
+
subscribe({id, fn}) {
|
173
|
+
if (typeof id !== 'string' || id.length === 0 || typeof fn !== 'function') {
|
174
|
+
throw new Error([
|
175
|
+
'Config.subscribe: options must be an object with a non-empty string `id` ',
|
176
|
+
'(the subscription owner\'s id), and a callback function `fn`.'
|
177
|
+
].join(''))
|
178
|
+
}
|
179
|
+
|
180
|
+
const me = this;
|
181
|
+
|
182
|
+
if (!me.#subscribers[id]) {
|
183
|
+
me.#subscribers[id] = new Set()
|
184
|
+
}
|
185
|
+
|
186
|
+
me.#subscribers[id].add(fn);
|
187
|
+
|
188
|
+
return () => {
|
189
|
+
const subscriberSet = me.#subscribers[id];
|
190
|
+
if (subscriberSet) {
|
191
|
+
subscriberSet.delete(fn);
|
192
|
+
if (subscriberSet.size === 0) {
|
193
|
+
delete me.#subscribers[id]
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
136
197
|
}
|
137
198
|
}
|
138
199
|
|
200
|
+
Object.defineProperties(Config.prototype, {
|
201
|
+
clone: {
|
202
|
+
value: 'deep',
|
203
|
+
writable: false,
|
204
|
+
configurable: true,
|
205
|
+
enumerable: false
|
206
|
+
},
|
207
|
+
cloneOnGet: {
|
208
|
+
value: null,
|
209
|
+
writable: false,
|
210
|
+
configurable: true,
|
211
|
+
enumerable: false
|
212
|
+
},
|
213
|
+
isEqual: {
|
214
|
+
value: Neo.isEqual,
|
215
|
+
writable: false,
|
216
|
+
configurable: true,
|
217
|
+
enumerable: false
|
218
|
+
},
|
219
|
+
mergeStrategy: {
|
220
|
+
value: 'replace',
|
221
|
+
writable: false,
|
222
|
+
configurable: true,
|
223
|
+
enumerable: false
|
224
|
+
}
|
225
|
+
});
|
226
|
+
|
227
|
+
const ns = Neo.ns('Neo.core', true);
|
228
|
+
ns.Config = Config;
|
229
|
+
|
139
230
|
export default Config;
|