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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/ViewportController.mjs +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/MainContainerController.mjs +6 -6
- 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/{Collections.md → datahandling/Collections.md} +6 -6
- package/learn/guides/datahandling/Grids.md +621 -0
- package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
- package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
- package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
- package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
- package/learn/tree.json +64 -57
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +244 -88
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +35 -3
- package/src/component/Base.mjs +72 -61
- package/src/container/Base.mjs +28 -24
- package/src/controller/Base.mjs +87 -63
- package/src/core/Base.mjs +207 -33
- package/src/core/Compare.mjs +3 -13
- package/src/core/Config.mjs +230 -0
- package/src/core/ConfigSymbols.mjs +3 -0
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/core/Util.mjs +3 -18
- package/src/data/RecordFactory.mjs +22 -3
- 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/util/Function.mjs +52 -5
- 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/config/Basic.mjs +149 -0
- 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/learn/guides/ExtendingNeoClasses.md +0 -331
- /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
- /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
- /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
- /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
- /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
- /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
- /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
- /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
- /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
- /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
- /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
- /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
- /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
- /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
- /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
package/src/core/Base.mjs
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
import {buffer, debounce, intercept, resolveCallback, throttle} from '../util/Function.mjs';
|
2
|
-
import
|
2
|
+
import Compare from '../core/Compare.mjs';
|
3
|
+
import Util from '../core/Util.mjs';
|
4
|
+
import Config from './Config.mjs';
|
5
|
+
import {isDescriptor} from './ConfigSymbols.mjs';
|
6
|
+
import IdGenerator from './IdGenerator.mjs';
|
7
|
+
import EffectBatchManager from './EffectBatchManager.mjs';
|
3
8
|
|
4
9
|
const configSymbol = Symbol.for('configSymbol'),
|
5
10
|
forceAssignConfigs = Symbol('forceAssignConfigs'),
|
@@ -94,12 +99,19 @@ class Base {
|
|
94
99
|
* @protected
|
95
100
|
*/
|
96
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,
|
97
109
|
/**
|
98
110
|
* The config will get set to `true` once the Promise of `async initAsync()` is resolved.
|
99
111
|
* You can use `afterSetIsReady()` to get notified once the ready state is reached.
|
100
112
|
* Since not all classes use the Observable mixin, Neo will not fire an event.
|
101
113
|
* method body.
|
102
|
-
* @member {Boolean}
|
114
|
+
* @member {Boolean} isReady_=false
|
103
115
|
*/
|
104
116
|
isReady_: false,
|
105
117
|
/**
|
@@ -125,6 +137,18 @@ class Base {
|
|
125
137
|
remote_: null
|
126
138
|
}
|
127
139
|
|
140
|
+
/**
|
141
|
+
* A private field to store the Config controller instances.
|
142
|
+
* @member {Object} #configs={}
|
143
|
+
* @private
|
144
|
+
*/
|
145
|
+
#configs = {};
|
146
|
+
/**
|
147
|
+
* Internal cache for all config subscription cleanup functions.
|
148
|
+
* @member {Function[]} #configSubscriptionCleanups=[]
|
149
|
+
* @private
|
150
|
+
*/
|
151
|
+
#configSubscriptionCleanups = []
|
128
152
|
/**
|
129
153
|
* Internal cache for all timeout ids when using this.timeout()
|
130
154
|
* @member {Number[]} timeoutIds=[]
|
@@ -133,8 +157,39 @@ class Base {
|
|
133
157
|
#timeoutIds = []
|
134
158
|
|
135
159
|
/**
|
136
|
-
*
|
137
|
-
*
|
160
|
+
* The main initializer for all Neo.mjs classes, invoked by `Neo.create()`.
|
161
|
+
* NOTE: This is not the native `constructor()`, which is called without arguments by `Neo.create()` first.
|
162
|
+
*
|
163
|
+
* This method orchestrates the entire instance initialization process, including
|
164
|
+
* the setup of the powerful and flexible config system.
|
165
|
+
*
|
166
|
+
* The `config` parameter is a single object that can contain different types of properties,
|
167
|
+
* which are processed in a specific order to ensure consistency and predictability:
|
168
|
+
*
|
169
|
+
* 1. **Public Class Fields & Other Properties:** Any key in the `config` object that is NOT
|
170
|
+
* defined in the class's `static config` hierarchy is considered a public field or a
|
171
|
+
* dynamic property. These are assigned directly to the instance (`this.myField = value`)
|
172
|
+
* at the very beginning. This is crucial so that subsequent config hooks (like `afterSet*`)
|
173
|
+
* can access their latest values.
|
174
|
+
*
|
175
|
+
* 2. **Reactive Configs:** A property is considered reactive if it is defined with a trailing
|
176
|
+
* underscore (e.g., `myValue_`) in the `static config` of **any class in the inheritance
|
177
|
+
* chain**. Subclasses can provide new default values for these configs without the
|
178
|
+
* underscore, and they will still be reactive. Their values are applied via generated
|
179
|
+
* setters, triggering `beforeSet*` and `afterSet*` hooks, and they are wrapped in a
|
180
|
+
* `Neo.core.Config` instance to enable subscription-based reactivity.
|
181
|
+
*
|
182
|
+
* 3. **Non-Reactive Configs:** Properties defined in `static config` without a trailing
|
183
|
+
* underscore in their entire inheritance chain. Their default values are applied directly
|
184
|
+
* to the class **prototype**, making them shared across all instances and allowing for
|
185
|
+
* run-time modifications (prototypal inheritance). When a new value is passed to this
|
186
|
+
* method, it creates an instance-specific property that shadows the prototype value.
|
187
|
+
*
|
188
|
+
* This method also initializes the observable mixin (if applicable) and schedules asynchronous
|
189
|
+
* logic like `initAsync()` (which handles remote method access) to run after the synchronous
|
190
|
+
* construction chain is complete.
|
191
|
+
*
|
192
|
+
* @param {Object} config={} The initial configuration object for the instance.
|
138
193
|
*/
|
139
194
|
construct(config={}) {
|
140
195
|
let me = this;
|
@@ -152,13 +207,9 @@ class Base {
|
|
152
207
|
}
|
153
208
|
});
|
154
209
|
|
155
|
-
me.
|
210
|
+
me.id = config.id || IdGenerator.getId(this.getIdKey());
|
156
211
|
delete config.id;
|
157
212
|
|
158
|
-
if (me.constructor.config) {
|
159
|
-
delete me.constructor.config.id
|
160
|
-
}
|
161
|
-
|
162
213
|
me.getStaticConfig('observable') && me.initObservable(config);
|
163
214
|
|
164
215
|
// assign class field values prior to configs
|
@@ -202,7 +253,7 @@ class Base {
|
|
202
253
|
if (oldValue) {
|
203
254
|
if (hasManager) {
|
204
255
|
Neo.manager.Instance.unregister(oldValue)
|
205
|
-
} else {
|
256
|
+
} else if (Neo.idMap) {
|
206
257
|
delete Neo.idMap[oldValue]
|
207
258
|
}
|
208
259
|
}
|
@@ -212,7 +263,7 @@ class Base {
|
|
212
263
|
Neo.manager.Instance.register(me);
|
213
264
|
} else {
|
214
265
|
Neo.idMap ??= {};
|
215
|
-
Neo.idMap[
|
266
|
+
Neo.idMap[value] = me
|
216
267
|
}
|
217
268
|
}
|
218
269
|
}
|
@@ -342,16 +393,6 @@ class Base {
|
|
342
393
|
this.__proto__.constructor.overwrittenMethods[methodName].call(this, ...args)
|
343
394
|
}
|
344
395
|
|
345
|
-
/**
|
346
|
-
* Uses the IdGenerator to create an id if a static one is not explicitly set.
|
347
|
-
* Registers the instance to manager.Instance if this one is already created,
|
348
|
-
* otherwise stores it inside a tmp map.
|
349
|
-
* @param {String} id
|
350
|
-
*/
|
351
|
-
createId(id) {
|
352
|
-
this.id = id || IdGenerator.getId(this.getIdKey())
|
353
|
-
}
|
354
|
-
|
355
396
|
/**
|
356
397
|
* Unregisters this instance from Neo.manager.Instance
|
357
398
|
* and removes all object entries from this instance
|
@@ -359,10 +400,16 @@ class Base {
|
|
359
400
|
destroy() {
|
360
401
|
let me = this;
|
361
402
|
|
403
|
+
me.isDestroying = true;
|
404
|
+
|
362
405
|
me.#timeoutIds.forEach(id => {
|
363
406
|
clearTimeout(id)
|
364
407
|
});
|
365
408
|
|
409
|
+
me.#configSubscriptionCleanups.forEach(cleanup => {
|
410
|
+
cleanup()
|
411
|
+
});
|
412
|
+
|
366
413
|
if (Base.instanceManagerAvailable === true) {
|
367
414
|
Neo.manager.Instance.unregister(me)
|
368
415
|
} else if (Neo.idMap) {
|
@@ -386,6 +433,22 @@ class Base {
|
|
386
433
|
me.isDestroyed = true
|
387
434
|
}
|
388
435
|
|
436
|
+
/**
|
437
|
+
* A public method to access the underlying Config controller.
|
438
|
+
* This enables advanced interactions like subscriptions.
|
439
|
+
* @param {String} key The name of the config property (e.g., 'items').
|
440
|
+
* @returns {Config|undefined} The Config instance, or undefined if not found.
|
441
|
+
*/
|
442
|
+
getConfig(key) {
|
443
|
+
let me = this;
|
444
|
+
|
445
|
+
if (!me.#configs[key] && me.isConfig(key)) {
|
446
|
+
me.#configs[key] = new Config(me.constructor.configDescriptors?.[key])
|
447
|
+
}
|
448
|
+
|
449
|
+
return me.#configs[key]
|
450
|
+
}
|
451
|
+
|
389
452
|
/**
|
390
453
|
* Used inside createId() as the default value passed to the IdGenerator.
|
391
454
|
* Override this method as needed.
|
@@ -443,8 +506,9 @@ class Base {
|
|
443
506
|
|
444
507
|
me.isConfiguring = true;
|
445
508
|
Object.assign(me[configSymbol], me.mergeConfig(config, preventOriginalConfig));
|
509
|
+
delete me[configSymbol].id;
|
446
510
|
me.processConfigs();
|
447
|
-
me.isConfiguring = false
|
511
|
+
me.isConfiguring = false
|
448
512
|
}
|
449
513
|
|
450
514
|
/**
|
@@ -475,6 +539,19 @@ class Base {
|
|
475
539
|
return !this.isDestroyed
|
476
540
|
}
|
477
541
|
|
542
|
+
/**
|
543
|
+
* @param {String} key
|
544
|
+
* @returns {Boolean}
|
545
|
+
*/
|
546
|
+
isConfig(key) {
|
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
|
550
|
+
// AND it is present as a defined config in the merged static config hierarchy.
|
551
|
+
// Neo.setupClass() removes the underscore from the static config keys.
|
552
|
+
return me.#configs[key] || (Neo.hasPropertySetter(me, key) && (key in me.constructor.config))
|
553
|
+
}
|
554
|
+
|
478
555
|
/**
|
479
556
|
* Override this method to change the order configs are applied to this instance.
|
480
557
|
* @param {Object} config
|
@@ -484,7 +561,8 @@ class Base {
|
|
484
561
|
*/
|
485
562
|
mergeConfig(config, preventOriginalConfig) {
|
486
563
|
let me = this,
|
487
|
-
ctor = me.constructor
|
564
|
+
ctor = me.constructor,
|
565
|
+
configDescriptors, staticConfig;
|
488
566
|
|
489
567
|
if (!ctor.config) {
|
490
568
|
throw new Error('Neo.applyClassConfig has not been run on ' + me.className)
|
@@ -494,7 +572,70 @@ class Base {
|
|
494
572
|
me.originalConfig = Neo.clone(config, true, true)
|
495
573
|
}
|
496
574
|
|
497
|
-
|
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
|
498
639
|
}
|
499
640
|
|
500
641
|
/**
|
@@ -615,24 +756,58 @@ class Base {
|
|
615
756
|
}
|
616
757
|
|
617
758
|
/**
|
618
|
-
*
|
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.
|
619
771
|
* @param {Object} values={}
|
620
772
|
*/
|
621
773
|
set(values={}) {
|
622
|
-
let me
|
774
|
+
let me = this,
|
775
|
+
classFieldsViaSet = {};
|
776
|
+
|
777
|
+
// Start batching for effects
|
778
|
+
EffectBatchManager.startBatch();
|
623
779
|
|
624
780
|
values = me.setFields(values);
|
625
781
|
|
626
782
|
// If the initial config processing is still running,
|
627
783
|
// finish this one first before dropping new values into the configSymbol.
|
628
|
-
//
|
784
|
+
// See: https://github.com/neomjs/neo/issues/2201
|
629
785
|
if (me[forceAssignConfigs] !== true && Object.keys(me[configSymbol]).length > 0) {
|
630
786
|
me.processConfigs()
|
631
787
|
}
|
632
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
|
633
798
|
Object.assign(me[configSymbol], values);
|
634
799
|
|
635
|
-
|
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();
|
636
811
|
}
|
637
812
|
|
638
813
|
/**
|
@@ -643,15 +818,14 @@ class Base {
|
|
643
818
|
* @protected
|
644
819
|
*/
|
645
820
|
setFields(config) {
|
646
|
-
let me
|
647
|
-
configNames = me.constructor.config;
|
821
|
+
let me = this;
|
648
822
|
|
649
823
|
Object.entries(config).forEach(([key, value]) => {
|
650
|
-
if (!
|
824
|
+
if (!me.isConfig(key) && !Neo.hasPropertySetter(me, key)) {
|
651
825
|
me[key] = value;
|
652
826
|
delete config[key]
|
653
827
|
}
|
654
|
-
})
|
828
|
+
});
|
655
829
|
|
656
830
|
return config
|
657
831
|
}
|
@@ -693,7 +867,7 @@ class Base {
|
|
693
867
|
* @returns {String}
|
694
868
|
*/
|
695
869
|
get [Symbol.toStringTag]() {
|
696
|
-
return
|
870
|
+
return this.className
|
697
871
|
}
|
698
872
|
|
699
873
|
/**
|
package/src/core/Compare.mjs
CHANGED
@@ -1,18 +1,7 @@
|
|
1
|
-
import Base from '../core/Base.mjs';
|
2
|
-
|
3
1
|
/**
|
4
2
|
* @class Neo.core.Compare
|
5
|
-
* @extends Neo.core.Base
|
6
3
|
*/
|
7
|
-
class Compare
|
8
|
-
static config = {
|
9
|
-
/**
|
10
|
-
* @member {String} className='Neo.core.Compare'
|
11
|
-
* @protected
|
12
|
-
*/
|
13
|
-
className: 'Neo.core.Compare'
|
14
|
-
}
|
15
|
-
|
4
|
+
class Compare {
|
16
5
|
/**
|
17
6
|
* Storing the comparison method names by data type
|
18
7
|
* @member {Object} map
|
@@ -174,7 +163,8 @@ class Compare extends Base {
|
|
174
163
|
}
|
175
164
|
}
|
176
165
|
|
177
|
-
|
166
|
+
const ns = Neo.ns('Neo.core', true);
|
167
|
+
ns.Compare = Compare;
|
178
168
|
|
179
169
|
// alias
|
180
170
|
Neo.isEqual = Compare.isEqual;
|
@@ -0,0 +1,230 @@
|
|
1
|
+
import EffectManager from './EffectManager.mjs';
|
2
|
+
import {isDescriptor} from './ConfigSymbols.mjs';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Represents an observable container for a config property.
|
6
|
+
* This class manages the value of a config, its subscribers, and custom behaviors
|
7
|
+
* like merge strategies and equality checks defined via a descriptor object.
|
8
|
+
*
|
9
|
+
* The primary purpose of this class is to enable fine-grained reactivity and
|
10
|
+
* decoupled cross-instance state sharing within the Neo.mjs framework.
|
11
|
+
* @class Neo.core.Config
|
12
|
+
* @private
|
13
|
+
* @internal
|
14
|
+
*/
|
15
|
+
class Config {
|
16
|
+
/**
|
17
|
+
* A Set to store callback functions that subscribe to changes in this config's value.
|
18
|
+
* @private
|
19
|
+
*/
|
20
|
+
#subscribers = {}
|
21
|
+
/**
|
22
|
+
* The internal value of the config property.
|
23
|
+
* @member #value
|
24
|
+
* @private
|
25
|
+
*/
|
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
|
+
*/
|
32
|
+
|
33
|
+
/**
|
34
|
+
* The cloning strategy to use when getting a value.
|
35
|
+
* Supported values: 'deep', 'shallow', 'none'.
|
36
|
+
* @member {String} cloneOnGet=null
|
37
|
+
*/
|
38
|
+
|
39
|
+
/**
|
40
|
+
* The function used to compare new and old values for equality.
|
41
|
+
* Defaults to `Neo.isEqual`. Can be overridden via a descriptor.
|
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'
|
49
|
+
*/
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Creates an instance of Config.
|
53
|
+
* @param {any|Object} configObject - The initial value for the config.
|
54
|
+
*/
|
55
|
+
constructor(configObject) {
|
56
|
+
if (Neo.isObject(configObject) && configObject[isDescriptor] === true) {
|
57
|
+
this.initDescriptor(configObject)
|
58
|
+
} else {
|
59
|
+
this.#value = configObject
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Gets the current value of the config property.
|
65
|
+
* @returns {any} The current value.
|
66
|
+
*/
|
67
|
+
get() {
|
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
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Initializes the `Config` instance using a descriptor object.
|
76
|
+
* Extracts `clone`, `mergeStrategy` and `isEqual` from the descriptor.
|
77
|
+
* The internal `#value` is NOT set by this method.
|
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.
|
83
|
+
* @param {Function} [descriptor.isEqual=Neo.isEqual] - The equality comparison function.
|
84
|
+
*/
|
85
|
+
initDescriptor({clone, cloneOnGet, isEqual, merge}) {
|
86
|
+
let me = this;
|
87
|
+
|
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
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* Notifies all subscribed callbacks about a change in the config's value.
|
115
|
+
* @param {any} newValue - The new value of the config.
|
116
|
+
* @param {any} oldValue - The old value of the config.
|
117
|
+
*/
|
118
|
+
notify(newValue, oldValue) {
|
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
|
+
}
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
/**
|
130
|
+
* Sets a new value for the config property.
|
131
|
+
* This method performs an equality check using `this.isEqual` before updating the value.
|
132
|
+
* If the value has changed, it updates `#value` and notifies all subscribers.
|
133
|
+
* @param {any} newValue - The new value to set.
|
134
|
+
* @returns {Boolean} True if the value changed, false otherwise.
|
135
|
+
*/
|
136
|
+
set(newValue) {
|
137
|
+
if (newValue === undefined) return false; // Preserve original behavior for undefined
|
138
|
+
|
139
|
+
const
|
140
|
+
me = this,
|
141
|
+
oldValue = me.#value;
|
142
|
+
|
143
|
+
// The setter automatically uses the configured equality check
|
144
|
+
if (!me.isEqual(newValue, oldValue)) {
|
145
|
+
me.#value = newValue;
|
146
|
+
me.notify(newValue, oldValue);
|
147
|
+
return true
|
148
|
+
}
|
149
|
+
|
150
|
+
return false
|
151
|
+
}
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Sets the internal value of the config property directly, without performing
|
155
|
+
* an equality check or notifying subscribers.
|
156
|
+
* This method is intended for internal framework use where direct assignment
|
157
|
+
* is necessary (e.g., during initial setup or specific internal optimizations).
|
158
|
+
* @param {any} newValue - The new value to set directly.
|
159
|
+
*/
|
160
|
+
setRaw(newValue) {
|
161
|
+
this.#value = newValue
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Subscribes a callback function to changes in this config's value.
|
166
|
+
* The callback will be invoked with `(newValue, oldValue)` whenever the config changes.
|
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.
|
170
|
+
* @returns {Function} A cleanup function to unsubscribe the callback.
|
171
|
+
*/
|
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
|
+
}
|
197
|
+
}
|
198
|
+
}
|
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
|
+
|
230
|
+
export default Config;
|