neo.mjs 10.0.0-alpha.3 → 10.0.0-alpha.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 (109) hide show
  1. package/.github/CODING_GUIDELINES.md +1 -1
  2. package/README.md +52 -11
  3. package/ServiceWorker.mjs +2 -2
  4. package/apps/portal/index.html +1 -1
  5. package/apps/portal/view/blog/List.mjs +1 -1
  6. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  7. package/apps/portal/view/learn/ContentComponent.mjs +2 -1
  8. package/apps/portal/view/learn/MainContainerStateProvider.mjs +3 -6
  9. package/apps/realworld/view/HomeComponent.mjs +1 -1
  10. package/apps/realworld/view/user/ProfileComponent.mjs +1 -1
  11. package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
  12. package/apps/shareddialog/view/MainContainerController.mjs +2 -2
  13. package/buildScripts/buildThemes.mjs +1 -1
  14. package/examples/grid/animatedRowSorting/Viewport.mjs +4 -4
  15. package/examples/grid/bigData/ControlsContainer.mjs +3 -3
  16. package/examples/grid/bigData/GridContainer.mjs +8 -8
  17. package/examples/grid/cellEditing/MainContainer.mjs +5 -5
  18. package/examples/grid/container/MainContainer.mjs +4 -4
  19. package/examples/grid/nestedRecordFields/Viewport.mjs +5 -5
  20. package/learn/README.md +83 -0
  21. package/learn/guides/ApplicationBootstrap.md +352 -0
  22. package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +500 -0
  23. package/learn/guides/WorkingWithVDom.md +748 -0
  24. package/learn/tree.json +53 -0
  25. package/package.json +2 -2
  26. package/resources/scss/src/grid/{View.scss → Body.scss} +2 -2
  27. package/resources/scss/src/grid/VerticalScrollbar.scss +1 -1
  28. package/resources/scss/src/grid/plugin/AnimateRows.scss +1 -1
  29. package/resources/scss/src/grid/plugin/CellEditing.scss +1 -1
  30. package/resources/scss/theme-dark/grid/{View.scss → Body.scss} +1 -1
  31. package/resources/scss/theme-light/grid/{View.scss → Body.scss} +1 -1
  32. package/resources/scss/theme-neo-light/grid/{View.scss → Body.scss} +1 -1
  33. package/src/DefaultConfig.mjs +27 -14
  34. package/src/Main.mjs +1 -1
  35. package/src/Neo.mjs +16 -0
  36. package/src/button/Base.mjs +2 -2
  37. package/src/calendar/view/MainContainerStateProvider.mjs +1 -1
  38. package/src/grid/{View.mjs → Body.mjs} +17 -17
  39. package/src/grid/Container.mjs +58 -58
  40. package/src/grid/ScrollManager.mjs +56 -56
  41. package/src/grid/VerticalScrollbar.mjs +2 -2
  42. package/src/grid/_export.mjs +2 -2
  43. package/src/grid/column/AnimatedChange.mjs +5 -5
  44. package/src/grid/column/Base.mjs +1 -1
  45. package/src/grid/column/Component.mjs +6 -6
  46. package/src/grid/header/Button.mjs +1 -1
  47. package/src/grid/header/Toolbar.mjs +9 -9
  48. package/src/grid/plugin/AnimateRows.mjs +1 -2
  49. package/src/layout/Cube.mjs +2 -2
  50. package/src/main/DeltaUpdates.mjs +11 -10
  51. package/src/main/addon/Navigator.mjs +1 -1
  52. package/src/main/addon/WindowPosition.mjs +1 -1
  53. package/src/main/render/StringBasedRenderer.mjs +1 -1
  54. package/src/tab/header/Toolbar.mjs +1 -1
  55. package/src/table/header/Button.mjs +1 -1
  56. package/src/toolbar/Base.mjs +1 -1
  57. package/src/util/Style.mjs +2 -6
  58. package/src/util/VDom.mjs +1 -1
  59. package/src/util/VNode.mjs +1 -1
  60. package/src/vdom/Helper.mjs +96 -49
  61. package/src/vdom/VNode.mjs +38 -2
  62. package/src/worker/App.mjs +8 -19
  63. package/src/worker/Base.mjs +87 -5
  64. package/src/worker/Manager.mjs +90 -36
  65. package/resources/data/deck/learnneo/tree.json +0 -50
  66. package/resources/data/deck/whyneo.md +0 -80
  67. /package/{resources/data/deck/learnneo/pages → learn}/Glossary.md +0 -0
  68. /package/{resources/data/deck/learnneo/pages → learn}/UsingTheseTopics.md +0 -0
  69. /package/{resources/data/deck/learnneo/pages → learn}/benefits/ConfigSystem.md +0 -0
  70. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Effort.md +0 -0
  71. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Features.md +0 -0
  72. /package/{resources/data/deck/learnneo/pages → learn}/benefits/FormsEngine.md +0 -0
  73. /package/{resources/data/deck/learnneo/pages → learn}/benefits/FourEnvironments.md +0 -0
  74. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Introduction.md +0 -0
  75. /package/{resources/data/deck/learnneo/pages → learn}/benefits/MultiWindow.md +0 -0
  76. /package/{resources/data/deck/learnneo/pages → learn}/benefits/OffTheMainThread.md +0 -0
  77. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Quick.md +0 -0
  78. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Speed.md +0 -0
  79. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/ComponentModels.md +0 -0
  80. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Config.md +0 -0
  81. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/DescribingTheUI.md +0 -0
  82. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Events.md +0 -0
  83. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Extending.md +0 -0
  84. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/References.md +0 -0
  85. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Setup.md +0 -0
  86. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Workspaces.md +0 -0
  87. /package/{resources/data/deck/learnneo/pages → learn}/guides/ComponentsAndContainers.md +0 -0
  88. /package/{resources/data/deck/learnneo/pages → learn}/guides/CustomComponents.md +0 -0
  89. /package/{resources/data/deck/learnneo/pages → learn}/guides/Forms.md +0 -0
  90. /package/{resources/data/deck/learnneo/pages → learn}/guides/InstanceLifecycle.md +0 -0
  91. /package/{resources/data/deck/learnneo/pages → learn}/guides/Layouts.md +0 -0
  92. /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonExample.md +0 -0
  93. /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonIntro.md +0 -0
  94. /package/{resources/data/deck/learnneo/pages → learn}/guides/Mixins.md +0 -0
  95. /package/{resources/data/deck/learnneo/pages → learn}/guides/MultiWindow.md +0 -0
  96. /package/{resources/data/deck/learnneo/pages → learn}/guides/PortalApp.md +0 -0
  97. /package/{resources/data/deck/learnneo/pages → learn}/guides/StateProviders.md +0 -0
  98. /package/{resources/data/deck/learnneo/pages → learn}/guides/Tables.md +0 -0
  99. /package/{resources/data/deck/learnneo/pages → learn}/guides/events/CustomEvents.md +0 -0
  100. /package/{resources/data/deck/learnneo/pages → learn}/guides/events/DomEvents.md +0 -0
  101. /package/{resources/data/deck/learnneo/pages → learn}/javascript/ClassFeatures.md +0 -0
  102. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Classes.md +0 -0
  103. /package/{resources/data/deck/learnneo/pages → learn}/javascript/NewNode.md +0 -0
  104. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Overrides.md +0 -0
  105. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Super.md +0 -0
  106. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/Earthquakes.md +0 -0
  107. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/RSP.md +0 -0
  108. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/TodoList.md +0 -0
  109. /package/resources/data/{deck/learnneo/data/theBeatles.json → theBeatles.json} +0 -0
@@ -37,6 +37,26 @@ class Helper extends Base {
37
37
  singleton: true
38
38
  }
39
39
 
40
+ /**
41
+ * @param {Object} config
42
+ */
43
+ construct(config) {
44
+ super.construct(config);
45
+
46
+ let me = this;
47
+
48
+ // Ensure Neo.currentWorker is defined before attaching listeners
49
+ Promise.resolve().then(async () => {
50
+ // Subscribe to global Neo.config changes for dynamic renderer switching.
51
+ Neo.currentWorker.on({
52
+ neoConfigChange: me.onNeoConfigChange,
53
+ scope : me
54
+ });
55
+
56
+ await me.importUtil()
57
+ })
58
+ }
59
+
40
60
  /**
41
61
  * @param {Object} config
42
62
  * @param {Object} config.deltas
@@ -65,11 +85,11 @@ class Helper extends Base {
65
85
  keys = Object.keys(vnode);
66
86
 
67
87
  Object.keys(oldVnode).forEach(prop => {
68
- if (!vnode.hasOwnProperty(prop)) {
88
+ if (!Object.hasOwn(vnode, prop)) {
69
89
  keys.push(prop)
70
- } else if (prop === 'attributes') { // find removed attributes
90
+ } else if (prop === 'attributes') { // Find removed attributes
71
91
  Object.keys(oldVnode[prop]).forEach(attr => {
72
- if (!vnode[prop].hasOwnProperty(attr)) {
92
+ if (!Object.hasOwn(vnode[prop], attr)) {
73
93
  vnode[prop][attr] = null
74
94
  }
75
95
  })
@@ -85,13 +105,22 @@ class Helper extends Base {
85
105
  attributes = {};
86
106
 
87
107
  Object.entries(value).forEach(([key, value]) => {
88
- if (!(oldVnode.attributes.hasOwnProperty(key) && oldVnode.attributes[key] === value)) {
89
- if (value !== null && !Neo.isString(value) && Neo.isEmpty(value)) {
90
- // ignore empty arrays & objects
91
- } else {
92
- attributes[key] = value
93
- }
108
+ const
109
+ oldValue = oldVnode.attributes[key],
110
+ hasOldValue = Object.hasOwn(oldVnode.attributes, key);
111
+
112
+ // If the attribute has an old value AND the value hasn't changed, skip.
113
+ if (hasOldValue && oldValue === value) {
114
+ return
115
+ }
116
+
117
+ // If the current value is null, or it's a non-string empty value (e.g., [], {}), skip.
118
+ // Note: An empty string ('') is a valid value and should NOT be skipped here.
119
+ if (value !== null && !Neo.isString(value) && Neo.isEmpty(value)) {
120
+ return
94
121
  }
122
+
123
+ attributes[key] = value
95
124
  });
96
125
 
97
126
  if (Object.keys(attributes).length > 0) {
@@ -148,7 +177,7 @@ class Helper extends Base {
148
177
  /**
149
178
  * Creates a Neo.vdom.VNode tree for the given vdom template.
150
179
  * The top level vnode contains the outerHTML as a string,
151
- * in case Neo.config.useStringBasedMounting === true
180
+ * in case Neo.config.useDomApiRenderer === false
152
181
  * @param {Object} opts
153
182
  * @param {String} opts.appName
154
183
  * @param {Boolean} [opts.autoMount]
@@ -156,22 +185,24 @@ class Helper extends Base {
156
185
  * @param {Number} opts.parentIndex
157
186
  * @param {Object} opts.vdom
158
187
  * @param {Number} opts.windowId
159
- * @returns {Promise<Object>}
188
+ * @returns {Object}
160
189
  */
161
- async create(opts) {
162
- let me = this,
190
+ create(opts) {
191
+ let me = this,
192
+ {util} = Neo.vdom,
163
193
  returnValue, vnode;
164
194
 
165
- await me.importDomApiVnodeCreator();
166
- await me.importStringFromVnode();
167
-
168
195
  vnode = me.createVnode(opts.vdom);
169
196
  returnValue = {...opts, vnode};
170
197
 
171
198
  delete returnValue.vdom;
172
199
 
173
- if (NeoConfig.useStringBasedMounting) {
174
- returnValue.outerHTML = Neo.vdom.util.StringFromVnode.create(vnode)
200
+ if (!NeoConfig.useDomApiRenderer) {
201
+ if (!util.StringFromVnode) {
202
+ throw new Error('VDom Helper render utilities are not loaded yet!')
203
+ }
204
+
205
+ returnValue.outerHTML = util.StringFromVnode.create(vnode)
175
206
  }
176
207
 
177
208
  return returnValue
@@ -303,10 +334,13 @@ class Helper extends Base {
303
334
  if (value !== undefined && value !== null && key !== 'flag' && key !== 'removeDom') {
304
335
  let hasUnit, newValue, style;
305
336
 
306
- switch(key) {
337
+ switch (key) {
307
338
  case 'tag':
308
339
  node.nodeName = value;
309
340
  break
341
+ case 'cls':
342
+ node.className = value;
343
+ break
310
344
  case 'html':
311
345
  node.innerHTML = value.toString(); // support for numbers
312
346
  break
@@ -333,13 +367,7 @@ class Helper extends Base {
333
367
 
334
368
  node.childNodes = newValue;
335
369
  break
336
- case 'cls':
337
- if (value && !Array.isArray(value)) {
338
- node.className = [value]
339
- } else if (!(Array.isArray(value) && value.length < 1)) {
340
- node.className = value
341
- }
342
- break
370
+
343
371
  case 'data':
344
372
  if (value && Neo.typeOf(value) === 'Object') {
345
373
  Object.entries(value).forEach(([key, val]) => {
@@ -472,24 +500,23 @@ class Helper extends Base {
472
500
  }
473
501
 
474
502
  /**
475
- * Only import for the DOM API based mount adapter.
503
+ * Imports either (if not already imported):
504
+ * `Neo.vdom.util.DomApiVnodeCreator` if Neo.config.useDomApiRenderer === true
505
+ * `Neo.vdom.util.StringFromVnode` if Neo.config.useDomApiRenderer === false
476
506
  * @returns {Promise<void>}
477
507
  * @protected
478
508
  */
479
- async importDomApiVnodeCreator() {
480
- if (!NeoConfig.useStringBasedMounting && !Neo.vdom.util?.DomApiVnodeCreator) {
481
- await import('./util/DomApiVnodeCreator.mjs')
482
- }
483
- }
509
+ async importUtil() {
510
+ const {util} = Neo.vdom;
484
511
 
485
- /**
486
- * Only import for the string based mount adapter.
487
- * @returns {Promise<void>}
488
- * @protected
489
- */
490
- async importStringFromVnode() {
491
- if (NeoConfig.useStringBasedMounting && !Neo.vdom.util?.StringFromVnode) {
492
- await import('./util/StringFromVnode.mjs')
512
+ if (NeoConfig.useDomApiRenderer) {
513
+ if (!util?.DomApiVnodeCreator) {
514
+ await import('./util/DomApiVnodeCreator.mjs')
515
+ }
516
+ } else {
517
+ if (!util?.StringFromVnode) {
518
+ await import('./util/StringFromVnode.mjs')
519
+ }
493
520
  }
494
521
  }
495
522
 
@@ -514,12 +541,12 @@ class Helper extends Base {
514
541
 
515
542
  Object.assign(delta, {hasLeadingTextChildren, index: physicalIndex});
516
543
 
517
- if (NeoConfig.useStringBasedMounting) {
518
- // For string-based mounting, pass a string excluding moved nodes
519
- delta.outerHTML = Neo.vdom.util.StringFromVnode.create(vnode, movedNodes)
520
- } else {
544
+ if (NeoConfig.useDomApiRenderer) {
521
545
  // For direct DOM API mounting, pass the pruned VNode tree
522
546
  delta.vnode = Neo.vdom.util.DomApiVnodeCreator.create(vnode, movedNodes)
547
+ } else {
548
+ // For string-based mounting, pass a string excluding moved nodes
549
+ delta.outerHTML = Neo.vdom.util.StringFromVnode.create(vnode, movedNodes)
523
550
  }
524
551
 
525
552
  deltas.default.push(delta);
@@ -603,6 +630,18 @@ class Helper extends Base {
603
630
  this.createDeltas({deltas, oldVnode: movedNode.vnode, oldVnodeMap, vnode, vnodeMap})
604
631
  }
605
632
 
633
+ /**
634
+ * Handler for global Neo.config changes.
635
+ * If 'useDomApiRenderer' property changes, this method dynamically loads/clears the renderer utilities.
636
+ * @param {Object} config
637
+ * @return {Promise<void>}
638
+ */
639
+ async onNeoConfigChange(config) {
640
+ if(Object.hasOwn(config, 'useDomApiRenderer')) {
641
+ await this.importUtil()
642
+ }
643
+ }
644
+
606
645
  /**
607
646
  * @param {Object} config
608
647
  * @param {Object} config.deltas
@@ -633,14 +672,22 @@ class Helper extends Base {
633
672
  * @param {Object} opts
634
673
  * @param {Object} opts.vdom
635
674
  * @param {Object} opts.vnode
636
- * @returns {Promise<Object>}
675
+ * @returns {Object}
637
676
  */
638
- async update(opts) {
639
- let me = this,
677
+ update(opts) {
678
+ let me = this,
679
+ {util} = Neo.vdom,
640
680
  deltas, vnode;
641
681
 
642
- await me.importDomApiVnodeCreator();
643
- await me.importStringFromVnode();
682
+ if (NeoConfig.useDomApiRenderer) {
683
+ if (!util.DomApiVnodeCreator) {
684
+ throw new Error('Neo.vdom.Helper: DomApiVnodeCreator is not loaded yet for updates!')
685
+ }
686
+ } else {
687
+ if (!util.StringFromVnode) {
688
+ throw new Error('Neo.vdom.Helper: StringFromVnode is not loaded yet for updates!');
689
+ }
690
+ }
644
691
 
645
692
  vnode = me.createVnode(opts.vdom);
646
693
  deltas = me.createDeltas({oldVnode: opts.vnode, vnode});
@@ -76,7 +76,7 @@ class VNode {
76
76
  } else {
77
77
  Object.assign(me, {
78
78
  attributes: config.attributes || {},
79
- className : config.className || [],
79
+ className : normalizeClassName(config.className),
80
80
  nodeName : config.nodeName || 'div',
81
81
  style : config.style
82
82
  });
@@ -88,7 +88,7 @@ class VNode {
88
88
 
89
89
  // We only apply textContent, in case it has content
90
90
  else if (Object.hasOwn(config, 'textContent')) {
91
- me.textContent = Neo.config.useStringBasedMounting ? StringUtil.escapeHtml(textContent) : textContent
91
+ me.textContent = Neo.config.useDomApiRenderer ? textContent : StringUtil.escapeHtml(textContent)
92
92
  }
93
93
  }
94
94
 
@@ -99,6 +99,42 @@ class VNode {
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * vdom cls definitions might contain spaces, especially when it comes to iconCls.
104
+ * @example: myVdom = {cls: ['my-button', 'fa fa-user']}
105
+ *
106
+ * On DOM level, classList.add() will throw, in case it gets an input containing a space.
107
+ *
108
+ * This is a module-scoped utility function, not a method of the VNode class.
109
+ * VNodes are transferred via structured cloning (e.g., in postMessage()), which strips methods.
110
+ * Keeping this logic separate from the VNode class itself ensures conceptual purity and a cleaner data model,
111
+ * as methods defined on the VNode instance would be lost during transfer anyway.
112
+ *
113
+ * @param {String|String[]} classNameInput
114
+ * @returns {String[]}
115
+ * @private
116
+ */
117
+ function normalizeClassName(classNameInput) {
118
+ let normalizedClasses = [];
119
+
120
+ if (Neo.isString(classNameInput)) {
121
+ normalizedClasses = classNameInput.split(' ').filter(Boolean)
122
+ } else if (Array.isArray(classNameInput)) {
123
+ classNameInput.forEach(cls => {
124
+ if (Neo.isString(cls)) {
125
+ if (cls.includes(' ')) {
126
+ normalizedClasses.push(...cls.split(' ').filter(Boolean))
127
+ } else if (cls !== '') {
128
+ normalizedClasses.push(cls)
129
+ }
130
+ }
131
+ })
132
+ }
133
+
134
+ // Remove duplicates if necessary
135
+ return [...new Set(normalizedClasses)]
136
+ }
137
+
102
138
  const ns = Neo.ns('Neo.vdom', true);
103
139
  ns.VNode = VNode;
104
140
 
@@ -35,7 +35,8 @@ class App extends Base {
35
35
  'destroyNeoInstance',
36
36
  'fireEvent',
37
37
  'getConfigs',
38
- 'setConfigs'
38
+ 'setConfigs',
39
+ 'setGlobalConfig' // points to worker.Base: setGlobalConfig()
39
40
  ]
40
41
  },
41
42
  /**
@@ -45,11 +46,6 @@ class App extends Base {
45
46
  singleton: true
46
47
  }
47
48
 
48
- /**
49
- * @member {Object|null} data=null
50
- * @protected
51
- */
52
- data = null
53
49
  /**
54
50
  * @member {Boolean} isUsingStateProviders=false
55
51
  * @protected
@@ -387,23 +383,16 @@ class App extends Base {
387
383
  * @param {Object} data
388
384
  */
389
385
  onLoadApplication(data) {
390
- let me = this,
391
- {config} = Neo,
392
- app, path;
393
-
394
- if (data) {
395
- me.data = data;
396
- config.resourcesPath = data.resourcesPath
397
- }
398
-
399
- path = me.data.path;
386
+ let me = this,
387
+ {config} = Neo,
388
+ {appPath} = config;
400
389
 
401
390
  if (config.environment !== 'development') {
402
- path = path.startsWith('/') ? path.substring(1) : path
391
+ appPath = appPath.startsWith('/') ? appPath.substring(1) : appPath
403
392
  }
404
393
 
405
- me.importApp(path).then(module => {
406
- app = module.onStart();
394
+ me.importApp(appPath).then(module => {
395
+ module.onStart();
407
396
 
408
397
  // short delay to ensure Component Controllers are ready
409
398
  config.hash && me.timeout(5).then(() => {
@@ -71,8 +71,9 @@ class Worker extends Base {
71
71
  gt.onmessage = me.onMessage.bind(me)
72
72
  }
73
73
 
74
- Neo.currentWorker = me;
75
- Neo.workerId = me.workerId
74
+ Neo.currentWorker = me;
75
+ Neo.setGlobalConfig = me.setGlobalConfig.bind(me);
76
+ Neo.workerId = me.workerId
76
77
  }
77
78
 
78
79
  /**
@@ -220,10 +221,17 @@ class Worker extends Base {
220
221
  }
221
222
 
222
223
  /**
223
- * @param {Object} msg
224
+ * Handles the initial registration of the `Neo.config` for this worker's realm.
225
+ * Triggered when receiving a worker message with `{action: 'registerNeoConfig'}` from the Main Thread's `Neo.worker.Manager`.
226
+ * This method is primarily responsible for setting the initial global `Neo.config` object in this worker's scope
227
+ * upon its creation. It also handles associating `windowId` with `MessagePort`s for Shared Workers.
228
+ *
229
+ * @param {Object} msg The incoming message object.
230
+ * @param {Object} msg.data The initial global Neo.config data object.
231
+ * @param {Number} msg.data.windowId The unique ID of the window/tab (relevant for SharedWorkers).
224
232
  */
225
233
  onRegisterNeoConfig(msg) {
226
- Neo.config = Neo.config || {};
234
+ Neo.ns('Neo.config', true);
227
235
 
228
236
  let me = this,
229
237
  {windowId} = msg.data,
@@ -236,7 +244,29 @@ class Worker extends Base {
236
244
  }
237
245
  }
238
246
 
239
- Object.assign(Neo.config, msg.data)
247
+ Neo.merge(Neo.config, msg.data)
248
+ }
249
+
250
+ /**
251
+ * Handles runtime updates to the global `Neo.config` for this worker's realm.
252
+ * This method is triggered when receiving a worker message with `{action: 'setNeoConfig'}`
253
+ * from the Main Thread's `Neo.worker.Manager`. This message signifies a global config change
254
+ * that originated either from this worker's Main Thread or was broadcast from another
255
+ * connected browser window via a Shared Worker.
256
+ *
257
+ * It merges the incoming configuration changes into this worker's local `Neo.config`
258
+ * and fires a local `neoConfigChange` event, allowing other instances within this worker
259
+ * to react to the updated configuration.
260
+ *
261
+ * @param {Object} msg The destructured arguments from the message payload.
262
+ * @param {Object} msg.config The partial or full `Neo.config` object to merge.
263
+ */
264
+ onSetNeoConfig({config}) {
265
+ let me = this;
266
+
267
+ Neo.merge(Neo.config, config);
268
+
269
+ me.fire('neoConfigChange', config)
240
270
  }
241
271
 
242
272
  /**
@@ -307,6 +337,58 @@ class Worker extends Base {
307
337
 
308
338
  return message
309
339
  }
340
+
341
+ /**
342
+ * Initiates a global Neo.config change from a worker's context.
343
+ * This method is exposed globally as `Neo.setGlobalConfig` within each worker realm.
344
+ *
345
+ * It orchestrates the propagation of the config change to the Main Thread
346
+ * and, if a Shared Worker is active, across all connected browser windows,
347
+ * ensuring a single, consistent Neo.config state everywhere.
348
+ *
349
+ * You can pass a partial config object to update specific keys.
350
+ * For nested objects, Neo.mjs performs a deep merge.
351
+ *
352
+ * @param {Object} config The partial or full Neo.config object with changes to apply.
353
+ */
354
+ setGlobalConfig(config) {
355
+ const
356
+ me = this,
357
+ {Manager} = Neo.worker; // Remote access proxy object
358
+
359
+ // Apply the config change locally to this worker's Neo.config and
360
+ // trigger its local change events immediately. This ensures immediate
361
+ // feedback and an updated state for the worker that initiated the change.
362
+ me.onSetNeoConfig({config});
363
+
364
+ if (me.isSharedWorker) {
365
+ // This block executes when the calling worker instance is a Shared Worker.
366
+ // This happens if `Neo.config.useSharedWorkers` is true, meaning App, VDom,
367
+ // Data, Canvas, and Task workers are all SharedWorker instances.
368
+ // This Shared Worker (the one where setGlobalConfig was called) acts as the
369
+ // central point to inform all connected Main Threads (browser windows).
370
+ me.ports.forEach((port, index) => {
371
+ // Send the config change to each connected Main Thread.
372
+ // The `broadcast` flag is crucial here for the *receiving* Main Thread:
373
+ // - `broadcast: true` (for the first port/Main Thread in the list): This Main Thread
374
+ // will apply the config locally and is then responsible for propagating it to *all*
375
+ // its own associated Shared Workers connected to that Main Thread),
376
+ // **excluding the worker that originated this change**.
377
+ // - `broadcast: false` (for all other ports/Main Threads): These Main Threads
378
+ // will simply apply the config locally and stop. They are passive recipients
379
+ // of the broadcast, synchronizing their state without initiating further actions back.
380
+ // The `excludeOrigin` parameter ensures the originating worker doesn't receive a redundant broadcast.
381
+ Manager.setNeoConfig({broadcast: index < 1, config, excludeOrigin: me.workerId, windowId: port.windowId})
382
+ })
383
+ } else {
384
+ // This Dedicated Worker (the one where setGlobalConfig was called) informs
385
+ // its single, connected Main Thread. The Main Thread will then:
386
+ // 1. Apply the config locally.
387
+ // 2. Broadcast this change to *all* other Dedicated Workers connected to
388
+ // *that same Main Thread*, **excluding the sender worker itself**.
389
+ Manager.setNeoConfig({broadcast: true, config, excludeOrigin: me.workerId})
390
+ }
391
+ }
310
392
  }
311
393
 
312
394
  export default Neo.setupClass(Worker);