neo.mjs 10.0.0-beta.6 → 10.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 (51) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
  2. package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
  3. package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
  4. package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
  5. package/ServiceWorker.mjs +2 -2
  6. package/apps/portal/index.html +1 -1
  7. package/apps/portal/view/ViewportController.mjs +6 -4
  8. package/apps/portal/view/examples/List.mjs +28 -19
  9. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  10. package/examples/functional/button/base/MainContainer.mjs +207 -0
  11. package/examples/functional/button/base/app.mjs +6 -0
  12. package/examples/functional/button/base/index.html +11 -0
  13. package/examples/functional/button/base/neo-config.json +6 -0
  14. package/learn/blog/v10-deep-dive-functional-components.md +293 -0
  15. package/learn/blog/v10-deep-dive-reactivity.md +522 -0
  16. package/learn/blog/v10-deep-dive-state-provider.md +432 -0
  17. package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
  18. package/learn/blog/v10-post1-love-story.md +383 -0
  19. package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
  20. package/package.json +3 -3
  21. package/src/DefaultConfig.mjs +2 -2
  22. package/src/Neo.mjs +47 -45
  23. package/src/component/Abstract.mjs +412 -0
  24. package/src/component/Base.mjs +18 -380
  25. package/src/core/Base.mjs +34 -33
  26. package/src/core/Effect.mjs +30 -34
  27. package/src/core/EffectManager.mjs +101 -14
  28. package/src/core/Observable.mjs +69 -65
  29. package/src/form/field/Text.mjs +11 -5
  30. package/src/functional/button/Base.mjs +384 -0
  31. package/src/functional/component/Base.mjs +51 -145
  32. package/src/layout/Cube.mjs +8 -4
  33. package/src/manager/VDomUpdate.mjs +179 -94
  34. package/src/mixin/VdomLifecycle.mjs +4 -1
  35. package/src/state/Provider.mjs +41 -27
  36. package/src/util/VDom.mjs +11 -4
  37. package/src/util/vdom/TreeBuilder.mjs +38 -62
  38. package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
  39. package/test/siesta/siesta.js +15 -3
  40. package/test/siesta/tests/VdomCalendar.mjs +7 -7
  41. package/test/siesta/tests/VdomHelper.mjs +7 -7
  42. package/test/siesta/tests/classic/Button.mjs +113 -0
  43. package/test/siesta/tests/core/EffectBatching.mjs +46 -41
  44. package/test/siesta/tests/functional/Button.mjs +113 -0
  45. package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
  46. package/test/siesta/tests/vdom/Advanced.mjs +14 -8
  47. package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
  48. package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
  49. package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
  50. package/test/siesta/tests/vdom/table/Container.mjs +9 -5
  51. package/src/core/EffectBatchManager.mjs +0 -67
@@ -1,6 +1,34 @@
1
1
  import Collection from '../collection/Base.mjs';
2
2
 
3
3
  /**
4
+ * The VDomUpdate manager is a singleton responsible for orchestrating and optimizing
5
+ * component VDOM updates within the Neo.mjs framework. It acts as a central coordinator
6
+ * to optimize the VDOM update process. Its primary goal is to reduce the amount of
7
+ * message roundtrips between the application and VDOM workers by aggregating multiple
8
+ * component updates into a single, optimized VDOM tree.
9
+ *
10
+ * Key Responsibilities:
11
+ * 1. **Update Merging & Aggregation:** Allows a parent component to absorb the update
12
+ * requests of its children. Instead of each child triggering a separate VDOM update
13
+ * message to the VDOM worker, the parent sends a single, aggregated VDOM tree. This
14
+ * significantly reduces the overhead of worker communication and can result in smaller,
15
+ * more focused data for the VDOM worker to process. While the amount of final DOM
16
+ * modifications remains the same, this aggregation is key to performance.
17
+ *
18
+ * 2. **Asynchronous Flow Control:** Manages the asynchronous nature of VDOM updates, which
19
+ * are often processed in a worker thread. It ensures that code awaiting an update
20
+ * (e.g., via a returned Promise) is correctly notified upon completion.
21
+ *
22
+ * 3. **Dependency Chaining:** Provides a "post-update" queue, allowing one component's
23
+ * update to be declaratively chained to another's, ensuring a predictable order of
24
+ * operations.
25
+ *
26
+ * 4. **State Tracking:** Keeps track of updates that are "in-flight" (i.e., currently
27
+ * being processed), which helps to avoid race conditions and redundant work.
28
+ *
29
+ * By centralizing these concerns, VDomUpdate plays a critical role in the framework's
30
+ * performance and rendering efficiency.
31
+ *
4
32
  * @class Neo.manager.VDomUpdate
5
33
  * @extends Neo.collection.Base
6
34
  * @singleton
@@ -13,34 +41,68 @@ class VDomUpdate extends Collection {
13
41
  */
14
42
  className: 'Neo.manager.VDomUpdate',
15
43
  /**
16
- * @member {Boolean} singleton=true
17
- * @protected
18
- */
19
- singleton: true,
20
- /**
44
+ * A collection that maps a parent component's ID (`ownerId`) to the set of child
45
+ * components whose VDOM updates have been merged into that parent's update cycle.
46
+ *
47
+ * The structure for each entry is:
48
+ * `{ ownerId: 'parent-id', children: Map<'child-id', {childUpdateDepth, distance}> }`
49
+ *
50
+ * - `ownerId`: The `id` of the parent component taking responsibility for the update.
51
+ * - `children`: A Map where keys are the `id`s of the merged children and values
52
+ * are objects containing metadata needed to calculate the total update scope.
53
+ *
21
54
  * @member {Neo.collection.Base|null} mergedCallbackMap=null
22
55
  * @protected
23
56
  */
24
57
  mergedCallbackMap: null,
25
58
  /**
59
+ * A collection that queues components that need to be updated immediately after
60
+ * another component's update cycle completes. This is used to handle rendering
61
+ * dependencies.
62
+ *
63
+ * The structure for each entry is:
64
+ * `{ ownerId: 'component-id', children: [{childId, resolve}] }`
65
+ *
66
+ * - `ownerId`: The `id` of the component whose update completion will trigger the queued updates.
67
+ * - `children`: An array of objects, where `childId` is the component to update and
68
+ * `resolve` is the Promise resolver to call after that subsequent update is done.
69
+ *
26
70
  * @member {Neo.collection.Base|null} postUpdateQueueMap=null
27
71
  * @protected
28
72
  */
29
73
  postUpdateQueueMap: null,
74
+ /**
75
+ * @member {Boolean} singleton=true
76
+ * @protected
77
+ */
78
+ singleton: true
30
79
  }
31
80
 
32
81
  /**
82
+ * A Map that tracks VDOM updates that have been dispatched to the VDOM worker but
83
+ * have not yet completed. This prevents redundant updates for the same component.
84
+ *
85
+ * The structure is: `Map<'component-id', updateDepth>`
86
+ *
33
87
  * @member {Map|null} inFlightUpdateMap=null
34
88
  * @protected
35
89
  */
36
90
  inFlightUpdateMap = null;
37
91
  /**
92
+ * A Map that stores Promise `resolve` functions associated with a component's update.
93
+ * When a component's VDOM update is finalized, the callbacks for its ID are executed,
94
+ * resolving the Promise returned by the component's `update()` method.
95
+ *
96
+ * The structure is: `Map<'component-id', [callback1, callback2, ...]>`
97
+ *
38
98
  * @member {Map|null} promiseCallbackMap=null
39
99
  * @protected
40
100
  */
41
101
  promiseCallbackMap = null;
42
102
 
43
103
  /**
104
+ * Initializes the manager's internal collections and maps.
105
+ * This is called automatically when the singleton instance is created.
44
106
  * @param {Object} config
45
107
  */
46
108
  construct(config) {
@@ -55,8 +117,11 @@ class VDomUpdate extends Collection {
55
117
  }
56
118
 
57
119
  /**
58
- * @param {String} ownerId
59
- * @param {Function} callback
120
+ * Registers a callback function to be executed when a specific component's
121
+ * VDOM update completes. This is the mechanism that resolves the Promise
122
+ * returned by `Component#update()`.
123
+ * @param {String} ownerId The `id` of the component owning the update.
124
+ * @param {Function} callback The function to execute upon completion.
60
125
  */
61
126
  addPromiseCallback(ownerId, callback) {
62
127
  let me = this;
@@ -69,72 +134,53 @@ class VDomUpdate extends Collection {
69
134
  }
70
135
 
71
136
  /**
72
- * Registers an update that is currently in-flight to the worker.
73
- * @param {String} ownerId The ID of the component owning the update.
74
- * @param {Number} updateDepth The depth of the in-flight update.
75
- */
76
- registerInFlightUpdate(ownerId, updateDepth) {
77
- this.inFlightUpdateMap.set(ownerId, updateDepth);
78
- }
79
-
80
- /**
81
- * Retrieves the update depth for an in-flight update.
82
- * @param {String} ownerId The ID of the component owning the update.
83
- * @returns {Number|undefined} The update depth, or undefined if not found.
84
- */
85
- getInFlightUpdateDepth(ownerId) {
86
- return this.inFlightUpdateMap.get(ownerId);
87
- }
88
-
89
- /**
90
- * Unregisters an in-flight update once it has completed.
91
- * @param {String} ownerId The ID of the component owning the update.
92
- */
93
- unregisterInFlightUpdate(ownerId) {
94
- this.inFlightUpdateMap.delete(ownerId);
95
- }
96
-
97
- /**
98
- * @param {String} ownerId
99
- * @param {String} childId
100
- * @param {Number} childUpdateDepth
101
- * @param {Number} distance
137
+ * Executes all callbacks associated with a completed VDOM update for a given `ownerId`.
138
+ * This method first processes callbacks for any children that were merged into this
139
+ * update cycle, then executes the callbacks for the `ownerId` itself.
140
+ * @param {String} ownerId The `id` of the component whose update has just completed.
141
+ * @param {Object} [data] Optional data to pass to the callbacks.
102
142
  */
103
- registerMerged(ownerId, childId, childUpdateDepth, distance) {
104
- let me = this,
105
- item = me.mergedCallbackMap.get(ownerId);
143
+ executeCallbacks(ownerId, data) {
144
+ let me = this,
145
+ item = me.mergedCallbackMap.get(ownerId),
146
+ callbackData = data ? [data] : [];
106
147
 
107
- if (!item) {
108
- item = {ownerId, children: new Map()};
109
- me.mergedCallbackMap.add(item);
148
+ if (item) {
149
+ item.children.forEach((value, key) => {
150
+ me.executePromiseCallbacks(key, ...callbackData)
151
+ });
152
+ me.mergedCallbackMap.remove(item);
110
153
  }
111
154
 
112
- item.children.set(childId, {childUpdateDepth, distance});
155
+ me.executePromiseCallbacks(ownerId, ...callbackData)
113
156
  }
114
157
 
115
158
  /**
116
- * @param {String} ownerId
117
- * @param {String} childId
118
- * @param {Function} [resolve]
159
+ * A helper method that invokes all registered promise callbacks for a given
160
+ * component ID and then clears them from the queue.
161
+ * @param {String} ownerId The `id` of the component.
162
+ * @param {Object} [data] Optional data to pass to the callbacks.
119
163
  */
120
- registerPostUpdate(ownerId, childId, resolve) {
121
- let me = this,
122
- item = me.postUpdateQueueMap.get(ownerId),
123
- childCallbacks;
124
-
125
- if (!item) {
126
- item = {ownerId, children: []};
127
- me.postUpdateQueueMap.add(item);
128
- }
164
+ executePromiseCallbacks(ownerId, data) {
165
+ let me = this,
166
+ callbacks = me.promiseCallbackMap.get(ownerId);
129
167
 
130
- item.children.push({childId, resolve})
168
+ callbacks?.forEach(callback => callback(data));
169
+ me.promiseCallbackMap.delete(ownerId);
131
170
  }
132
171
 
133
172
  /**
134
- * Calculates the adjusted updateDepth for a parent component based on its merged children.
135
- * This method is called by the parent component right before it executes its own VDOM update.
136
- * @param {String} ownerId
137
- * @returns {Number|null} The adjusted update depth or null if no merged children are found
173
+ * Calculates the required `updateDepth` for a parent component based on its own
174
+ * needs and the needs of all child components whose updates have been merged into it.
175
+ * The final depth is the maximum required depth to ensure all changes are rendered.
176
+ *
177
+ * For example, if a parent needs to update its direct content (`updateDepth: 1`) but
178
+ * a merged child 3 levels down needs a full subtree update (`childUpdateDepth: -1`),
179
+ * this method will return -1, signaling a full recursive update from the parent.
180
+ *
181
+ * This method is called by the parent component right before it dispatches its VDOM update.
182
+ * @param {String} ownerId The `id` of the parent component.
183
+ * @returns {Number|null} The adjusted update depth, or `null` if no merged children exist.
138
184
  */
139
185
  getAdjustedUpdateDepth(ownerId) {
140
186
  let me = this,
@@ -146,71 +192,101 @@ class VDomUpdate extends Collection {
146
192
  if (item) {
147
193
  item.children.forEach(value => {
148
194
  if (value.childUpdateDepth === -1) {
149
- newDepth = -1;
195
+ newDepth = -1
150
196
  } else {
151
197
  // The new depth is the distance to the child plus the child's own required update depth.
152
- newDepth = value.distance + value.childUpdateDepth;
198
+ newDepth = value.distance + value.childUpdateDepth
153
199
  }
154
200
 
155
201
  if (newDepth === -1) {
156
- maxDepth = -1;
202
+ maxDepth = -1
157
203
  } else if (maxDepth !== -1) {
158
- maxDepth = Math.max(maxDepth, newDepth);
204
+ maxDepth = Math.max(maxDepth, newDepth)
159
205
  }
160
206
  });
161
207
 
162
- return maxDepth;
208
+ return maxDepth
163
209
  }
164
210
 
165
- return null;
211
+ return null
212
+ }
213
+
214
+ /**
215
+ * Retrieves the `updateDepth` for a component's update that is currently in-flight.
216
+ * @param {String} ownerId The `id` of the component owning the update.
217
+ * @returns {Number|undefined} The update depth, or `undefined` if no update is in-flight.
218
+ */
219
+ getInFlightUpdateDepth(ownerId) {
220
+ return this.inFlightUpdateMap.get(ownerId)
166
221
  }
167
222
 
168
223
  /**
169
- * Returns a Set of child IDs that have been merged into a parent's update.
170
- * @param {String} ownerId The ID of the parent component owning the update.
171
- * @returns {Set<String>|null} A Set containing the IDs of the merged children, or null.
224
+ * Returns a Set of child component IDs that have been merged into a parent's update cycle.
225
+ * This is used by the parent to know which children it is responsible for updating.
226
+ * @param {String} ownerId The `id` of the parent component.
227
+ * @returns {Set<String>|null} A Set containing the IDs of the merged children, or `null`.
172
228
  */
173
229
  getMergedChildIds(ownerId) {
174
230
  const item = this.mergedCallbackMap.get(ownerId);
175
231
  if (item) {
176
- return new Set(item.children.keys());
232
+ return new Set(item.children.keys())
177
233
  }
178
- return null;
234
+ return null
179
235
  }
180
236
 
181
237
  /**
182
- * @param {String} ownerId
183
- * @param {Object} [data]
238
+ * Marks a component's VDOM update as "in-flight," meaning it has been sent to the
239
+ * worker for processing.
240
+ * @param {String} ownerId The `id` of the component owning the update.
241
+ * @param {Number} updateDepth The depth of the in-flight update.
184
242
  */
185
- executeCallbacks(ownerId, data) {
186
- let me = this,
187
- item = me.mergedCallbackMap.get(ownerId),
188
- callbackData = data ? [data] : [];
243
+ registerInFlightUpdate(ownerId, updateDepth) {
244
+ this.inFlightUpdateMap.set(ownerId, updateDepth)
245
+ }
189
246
 
190
- if (item) {
191
- item.children.forEach((value, key) => {
192
- me.executePromiseCallbacks(key, ...callbackData)
193
- });
194
- me.mergedCallbackMap.remove(item);
247
+ /**
248
+ * Registers a child's update request to be merged into its parent's update cycle.
249
+ * This is called by a child component when it determines it can delegate its update
250
+ * to an ancestor.
251
+ * @param {String} ownerId The `id` of the parent component that will own the merged update.
252
+ * @param {String} childId The `id` of the child component requesting the merge.
253
+ * @param {Number} childUpdateDepth The update depth required by the child.
254
+ * @param {Number} distance The component tree distance (number of levels) between the parent and child.
255
+ */
256
+ registerMerged(ownerId, childId, childUpdateDepth, distance) {
257
+ let me = this,
258
+ item = me.mergedCallbackMap.get(ownerId);
259
+
260
+ if (!item) {
261
+ item = {ownerId, children: new Map()};
262
+ me.mergedCallbackMap.add(item)
195
263
  }
196
264
 
197
- me.executePromiseCallbacks(ownerId, ...callbackData)
265
+ item.children.set(childId, {childUpdateDepth, distance})
198
266
  }
199
267
 
200
268
  /**
201
- * @param {String} ownerId
202
- * @param {Object} [data]
269
+ * Queues a component update to be executed after another component's update is complete.
270
+ * @param {String} ownerId The `id` of the component to wait for.
271
+ * @param {String} childId The `id` of the component to update afterward.
272
+ * @param {Function} [resolve] The Promise resolver to be called when the `childId`'s subsequent update finishes.
203
273
  */
204
- executePromiseCallbacks(ownerId, data) {
205
- let me = this,
206
- callbacks = me.promiseCallbackMap.get(ownerId);
274
+ registerPostUpdate(ownerId, childId, resolve) {
275
+ let me = this,
276
+ item = me.postUpdateQueueMap.get(ownerId);
207
277
 
208
- callbacks?.forEach(callback => callback(data));
209
- me.promiseCallbackMap.delete(ownerId);
278
+ if (!item) {
279
+ item = {ownerId, children: []};
280
+ me.postUpdateQueueMap.add(item)
281
+ }
282
+
283
+ item.children.push({childId, resolve})
210
284
  }
211
285
 
212
286
  /**
213
- * @param {String} ownerId
287
+ * Triggers all pending updates that were queued to run after the specified `ownerId`'s
288
+ * update has completed.
289
+ * @param {String} ownerId The `id` of the component whose update has just finished.
214
290
  */
215
291
  triggerPostUpdates(ownerId) {
216
292
  let me = this,
@@ -223,13 +299,22 @@ class VDomUpdate extends Collection {
223
299
 
224
300
  if (component) {
225
301
  entry.resolve && me.addPromiseCallback(component.id, entry.resolve);
226
- component.update();
302
+ component.update()
227
303
  }
228
304
  });
229
305
 
230
- me.postUpdateQueueMap.remove(item);
306
+ me.postUpdateQueueMap.remove(item)
231
307
  }
232
308
  }
309
+
310
+ /**
311
+ * Removes a component's update from the "in-flight" registry. This is called after
312
+ * the VDOM worker confirms the update has been processed.
313
+ * @param {String} ownerId The `id` of the component owning the update.
314
+ */
315
+ unregisterInFlightUpdate(ownerId) {
316
+ this.inFlightUpdateMap.delete(ownerId)
317
+ }
233
318
  }
234
319
 
235
320
  export default Neo.setupClass(VDomUpdate);
@@ -440,6 +440,7 @@ class VdomLifecycle extends Base {
440
440
  * - you pass true for the mount param
441
441
  * - or the autoMount config is set to true
442
442
  * @param {Boolean} [mount] Mount the DOM after the vnode got created
443
+ * @returns {Promise<any>} If getting there, we return the data from vdom.Helper: create(), containing the vnode.
443
444
  */
444
445
  async render(mount) {
445
446
  let me = this,
@@ -486,7 +487,9 @@ class VdomLifecycle extends Base {
486
487
 
487
488
  autoMount && !useVdomWorker && me.mount();
488
489
 
489
- me.resolveVdomUpdate()
490
+ me.resolveVdomUpdate();
491
+
492
+ return data
490
493
  }
491
494
  }
492
495
 
@@ -2,7 +2,7 @@ import Base from '../core/Base.mjs';
2
2
  import ClassSystemUtil from '../util/ClassSystem.mjs';
3
3
  import Config from '../core/Config.mjs';
4
4
  import Effect from '../core/Effect.mjs';
5
- import EffectBatchManager from '../core/EffectBatchManager.mjs';
5
+ import EffectManager from '../core/EffectManager.mjs';
6
6
  import Observable from '../core/Observable.mjs';
7
7
  import {createHierarchicalDataProxy} from './createHierarchicalDataProxy.mjs';
8
8
  import {isDescriptor} from '../core/ConfigSymbols.mjs';
@@ -474,17 +474,6 @@ class Provider extends Base {
474
474
  internalSetData(key, value, originStateProvider) {
475
475
  const me = this;
476
476
 
477
- // If the value is a Neo.data.Record, treat it as an atomic value
478
- // and set it directly without further recursive processing of its properties.
479
- if (Neo.isRecord(value)) {
480
- const
481
- ownerDetails = me.getOwnerOfDataProperty(key),
482
- targetProvider = ownerDetails ? ownerDetails.owner : (originStateProvider || me);
483
-
484
- me.#setConfigValue(targetProvider, key, value, null);
485
- return
486
- }
487
-
488
477
  if (Neo.isObject(key)) {
489
478
  Object.entries(key).forEach(([dataKey, dataValue]) => {
490
479
  me.internalSetData(dataKey, dataValue, originStateProvider)
@@ -492,13 +481,28 @@ class Provider extends Base {
492
481
  return
493
482
  }
494
483
 
484
+ // Now 'key' is a string path.
485
+ // If 'value' is a plain object, we need to drill down further.
486
+ // If the value is a Neo.data.Record, treat it as an atomic value => it will not enter this block.
487
+ if (Neo.typeOf(value) === 'Object') {
488
+ Object.entries(value).forEach(([nestedKey, nestedValue]) => {
489
+ const fullPath = `${key}.${nestedKey}`;
490
+ me.internalSetData(fullPath, nestedValue, originStateProvider);
491
+ });
492
+ return // We've delegated the setting to deeper paths.
493
+ }
494
+
495
495
  const
496
496
  ownerDetails = me.getOwnerOfDataProperty(key),
497
497
  targetProvider = ownerDetails ? ownerDetails.owner : (originStateProvider || me);
498
498
 
499
499
  me.#setConfigValue(targetProvider, key, value, null);
500
500
 
501
- // Bubble up the change to parent configs to trigger their effects
501
+ // This is the "reactivity bubbling" logic. When a leaf property like 'user.name' changes,
502
+ // we must also trigger effects that depend on the parent object 'user'. We do this by
503
+ // creating a new object reference for each parent in the path. The spread syntax
504
+ // `{ ...oldParentValue, [leafKey]: latestValue }` is key, as it creates a new
505
+ // object, which the reactivity system detects as a change.
502
506
  let path = key,
503
507
  latestValue = value;
504
508
 
@@ -518,7 +522,11 @@ class Provider extends Base {
518
522
  break // Stop if parent is not an object
519
523
  }
520
524
  } else {
521
- break // Stop if parent config does not exist
525
+ // If the parent config doesn't exist, we need to create it to support bubbling.
526
+ // This is crucial for creating new nested data structures at runtime.
527
+ const newParentValue = {[leafKey]: latestValue};
528
+ me.#setConfigValue(targetProvider, path, newParentValue);
529
+ latestValue = newParentValue
522
530
  }
523
531
  }
524
532
  }
@@ -582,8 +590,8 @@ class Provider extends Base {
582
590
 
583
591
  /**
584
592
  * @param {Neo.component.Base} component
585
- * @param {String} configName
586
- * @param {String} storeName
593
+ * @param {String} configName
594
+ * @param {String} storeName
587
595
  */
588
596
  resolveStore(component, configName, storeName) {
589
597
  let store = this.getStore(storeName);
@@ -598,9 +606,9 @@ class Provider extends Base {
598
606
  * This method creates a new Config instance if one doesn't exist for the given path,
599
607
  * or updates an existing one. It also triggers binding effects and calls onDataPropertyChange.
600
608
  * @param {Neo.state.Provider} provider The StateProvider instance owning the config.
601
- * @param {String} path The full path of the data property (e.g., 'user.firstname').
602
- * @param {*} newValue The new value to set.
603
- * @param {*} oldVal The old value (optional, used for initial setup).
609
+ * @param {String} path The full path of the data property (e.g., 'user.firstname').
610
+ * @param {*} newValue The new value to set.
611
+ * @param {*} [oldVal] The old value (optional, used for initial setup).
604
612
  * @private
605
613
  */
606
614
  #setConfigValue(provider, path, newValue, oldVal) {
@@ -629,12 +637,15 @@ class Provider extends Base {
629
637
  * are run only once.
630
638
  *
631
639
  * @param {Object|String} key
632
- * @param {*} value
640
+ * @param {*} value
633
641
  */
634
642
  setData(key, value) {
635
- EffectBatchManager.startBatch();
636
- this.internalSetData(key, value, this);
637
- EffectBatchManager.endBatch()
643
+ EffectManager.pause();
644
+ try {
645
+ this.internalSetData(key, value, this)
646
+ } finally {
647
+ EffectManager.resume()
648
+ }
638
649
  }
639
650
 
640
651
  /**
@@ -645,12 +656,15 @@ class Provider extends Base {
645
656
  * are run only once.
646
657
  *
647
658
  * @param {Object|String} key
648
- * @param {*} value
659
+ * @param {*} value
649
660
  */
650
661
  setDataAtSameLevel(key, value) {
651
- EffectBatchManager.startBatch();
652
- this.internalSetData(key, value);
653
- EffectBatchManager.endBatch()
662
+ EffectManager.pause();
663
+ try {
664
+ this.internalSetData(key, value)
665
+ } finally {
666
+ EffectManager.resume()
667
+ }
654
668
  }
655
669
  }
656
670
 
package/src/util/VDom.mjs CHANGED
@@ -391,16 +391,23 @@ class VDom extends Base {
391
391
  vdom.id = vnode.id
392
392
  }
393
393
  } else {
394
- // we only want to change vdom ids in case there is not already an own id
395
- // (think of adding & removing nodes in parallel)
396
- if (!vdom.id && vnode.id) {
394
+ // We only want to add an ID if the vdom node does not already have one.
395
+ // This preserves developer-provided IDs while allowing the framework
396
+ // to assign IDs to nodes that need them for reconciliation.
397
+ // Also think of adding and removing nodes in parallel.
398
+ if (vnode.id && (!vdom.id || vdom.id.startsWith('neo-vnode-'))) {
397
399
  vdom.id = vnode.id
398
400
  }
399
401
  }
400
402
 
401
403
  if (childNodes) {
402
404
  cn = childNodes.map(item => VDom.getVdom(item));
403
- cn = cn.filter(item => item.removeDom !== true);
405
+ // The vnode.childNodes array is already filtered by the worker.
406
+ // We must filter the component's vdom.cn array identically to ensure
407
+ // both arrays are structurally aligned for the sync loop.
408
+ // The boolean check `item &&` is critical to remove falsy values
409
+ // from conditional rendering and prevent runtime errors.
410
+ cn = cn.filter(item => item && item.removeDom !== true);
404
411
  i = 0;
405
412
  len = cn?.length || 0;
406
413