neo.mjs 10.0.0-alpha.2 → 10.0.0-alpha.4

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 (85) 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/home/FooterContainer.mjs +1 -1
  6. package/apps/portal/view/learn/ContentComponent.mjs +2 -1
  7. package/apps/portal/view/learn/MainContainerStateProvider.mjs +3 -6
  8. package/apps/realworld/view/HomeComponent.mjs +1 -1
  9. package/apps/realworld/view/user/ProfileComponent.mjs +1 -1
  10. package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
  11. package/apps/shareddialog/view/MainContainerController.mjs +2 -2
  12. package/examples/component/gallery/ImageStore.mjs +2 -2
  13. package/examples/component/helix/ImageStore.mjs +2 -2
  14. package/learn/README.md +83 -0
  15. package/learn/guides/ApplicationBootstrap.md +354 -0
  16. package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +500 -0
  17. package/learn/guides/WorkingWithVDom.md +748 -0
  18. package/learn/tree.json +53 -0
  19. package/package.json +2 -2
  20. package/src/DefaultConfig.mjs +27 -14
  21. package/src/Main.mjs +1 -7
  22. package/src/Neo.mjs +16 -0
  23. package/src/button/Base.mjs +2 -2
  24. package/src/calendar/view/MainContainerStateProvider.mjs +1 -1
  25. package/src/grid/header/Button.mjs +1 -1
  26. package/src/layout/Cube.mjs +2 -2
  27. package/src/main/DeltaUpdates.mjs +20 -12
  28. package/src/main/addon/Navigator.mjs +1 -1
  29. package/src/main/addon/WindowPosition.mjs +1 -1
  30. package/src/main/render/StringBasedRenderer.mjs +1 -1
  31. package/src/tab/header/Toolbar.mjs +1 -1
  32. package/src/table/header/Button.mjs +1 -1
  33. package/src/toolbar/Base.mjs +1 -1
  34. package/src/util/VDom.mjs +1 -1
  35. package/src/util/VNode.mjs +1 -1
  36. package/src/vdom/Helper.mjs +96 -49
  37. package/src/vdom/VNode.mjs +38 -2
  38. package/src/worker/App.mjs +2 -1
  39. package/src/worker/Base.mjs +87 -5
  40. package/src/worker/Manager.mjs +86 -28
  41. package/resources/data/deck/learnneo/tree.json +0 -50
  42. package/resources/data/deck/whyneo.md +0 -80
  43. /package/{resources/data/deck/learnneo/pages → learn}/Glossary.md +0 -0
  44. /package/{resources/data/deck/learnneo/pages → learn}/UsingTheseTopics.md +0 -0
  45. /package/{resources/data/deck/learnneo/pages → learn}/benefits/ConfigSystem.md +0 -0
  46. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Effort.md +0 -0
  47. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Features.md +0 -0
  48. /package/{resources/data/deck/learnneo/pages → learn}/benefits/FormsEngine.md +0 -0
  49. /package/{resources/data/deck/learnneo/pages → learn}/benefits/FourEnvironments.md +0 -0
  50. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Introduction.md +0 -0
  51. /package/{resources/data/deck/learnneo/pages → learn}/benefits/MultiWindow.md +0 -0
  52. /package/{resources/data/deck/learnneo/pages → learn}/benefits/OffTheMainThread.md +0 -0
  53. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Quick.md +0 -0
  54. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Speed.md +0 -0
  55. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/ComponentModels.md +0 -0
  56. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Config.md +0 -0
  57. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/DescribingTheUI.md +0 -0
  58. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Events.md +0 -0
  59. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Extending.md +0 -0
  60. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/References.md +0 -0
  61. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Setup.md +0 -0
  62. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Workspaces.md +0 -0
  63. /package/{resources/data/deck/learnneo/pages → learn}/guides/ComponentsAndContainers.md +0 -0
  64. /package/{resources/data/deck/learnneo/pages → learn}/guides/CustomComponents.md +0 -0
  65. /package/{resources/data/deck/learnneo/pages → learn}/guides/Forms.md +0 -0
  66. /package/{resources/data/deck/learnneo/pages → learn}/guides/InstanceLifecycle.md +0 -0
  67. /package/{resources/data/deck/learnneo/pages → learn}/guides/Layouts.md +0 -0
  68. /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonExample.md +0 -0
  69. /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonIntro.md +0 -0
  70. /package/{resources/data/deck/learnneo/pages → learn}/guides/Mixins.md +0 -0
  71. /package/{resources/data/deck/learnneo/pages → learn}/guides/MultiWindow.md +0 -0
  72. /package/{resources/data/deck/learnneo/pages → learn}/guides/PortalApp.md +0 -0
  73. /package/{resources/data/deck/learnneo/pages → learn}/guides/StateProviders.md +0 -0
  74. /package/{resources/data/deck/learnneo/pages → learn}/guides/Tables.md +0 -0
  75. /package/{resources/data/deck/learnneo/pages → learn}/guides/events/CustomEvents.md +0 -0
  76. /package/{resources/data/deck/learnneo/pages → learn}/guides/events/DomEvents.md +0 -0
  77. /package/{resources/data/deck/learnneo/pages → learn}/javascript/ClassFeatures.md +0 -0
  78. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Classes.md +0 -0
  79. /package/{resources/data/deck/learnneo/pages → learn}/javascript/NewNode.md +0 -0
  80. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Overrides.md +0 -0
  81. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Super.md +0 -0
  82. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/Earthquakes.md +0 -0
  83. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/RSP.md +0 -0
  84. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/TodoList.md +0 -0
  85. /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
  /**
@@ -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);
@@ -27,11 +27,6 @@ class Manager extends Base {
27
27
  * @protected
28
28
  */
29
29
  className: 'Neo.worker.Manager',
30
- /**
31
- * @member {Boolean} singleton=true
32
- * @protected
33
- */
34
- singleton: true,
35
30
  /**
36
31
  * @member {Number} activeWorkers=0
37
32
  * @protected
@@ -51,12 +46,29 @@ class Manager extends Base {
51
46
  * @member {String[]|Neo.core.Base[]|null} mixins=[Observable, RemoteMethodAccess]
52
47
  */
53
48
  mixins: [Observable, RemoteMethodAccess],
49
+ /**
50
+ * Remote method access for other workers
51
+ * @member {Object} remote
52
+ * @protected
53
+ */
54
+ remote: {
55
+ app : ['setNeoConfig'],
56
+ canvas: ['setNeoConfig'],
57
+ data : ['setNeoConfig'],
58
+ task : ['setNeoConfig'],
59
+ vdom : ['setNeoConfig']
60
+ },
54
61
  /**
55
62
  * True in case the current browser supports window.SharedWorker.
56
63
  * @member {Boolean} sharedWorkersEnabled=false
57
64
  * @protected
58
65
  */
59
66
  sharedWorkersEnabled: false,
67
+ /**
68
+ * @member {Boolean} singleton=true
69
+ * @protected
70
+ */
71
+ singleton: true,
60
72
  /**
61
73
  * Internal flag to stop the worker communication in case their creation fails
62
74
  * @member {Boolean} stopCommunication=false
@@ -71,7 +83,7 @@ class Manager extends Base {
71
83
  */
72
84
  webWorkersEnabled: false,
73
85
  /**
74
- * Using the current timestamp as an unique window identifier
86
+ * Using the current timestamp as a unique window identifier
75
87
  * @member {Number} windowId=new Date().getTime()
76
88
  * @protected
77
89
  */
@@ -121,7 +133,8 @@ class Manager extends Base {
121
133
 
122
134
  !Neo.insideWorker && me.createWorkers();
123
135
 
124
- Neo.workerId = 'main';
136
+ Neo.setGlobalConfig = me.setGlobalConfig.bind(me);
137
+ Neo.workerId = 'main';
125
138
 
126
139
  me.promises = {};
127
140
 
@@ -136,18 +149,18 @@ class Manager extends Base {
136
149
 
137
150
  /**
138
151
  * Sends a message to each worker defined inside the this.workers config.
139
- * @param {Object} msg
152
+ * Only sends to workers that are currently active and available.
153
+ * @param {Object} msg The message payload to broadcast.
154
+ * @param {Object} [excludeOrigin] Optionally pass the origin realm name to exclude from the broadcast.
140
155
  */
141
- broadcast(msg) {
142
- Object.keys(this.workers).forEach(name => {
143
- if (!(
144
- name === 'canvas' && !NeoConfig.useCanvasWorker ||
145
- name === 'task' && !NeoConfig.useTaskWorker ||
146
- name === 'vdom' && !NeoConfig.useVdomWorker
147
- )) {
148
- this.sendMessage(name, msg)
156
+ broadcast(msg, excludeOrigin) {
157
+ let me = this;
158
+
159
+ Object.keys(me.workers).forEach(name => {
160
+ if (name !== excludeOrigin && me.getWorker(name)) {
161
+ me.sendMessage(name, msg)
149
162
  }
150
- });
163
+ })
151
164
  }
152
165
 
153
166
  /**
@@ -325,8 +338,10 @@ class Manager extends Base {
325
338
  data = data.data
326
339
  }
327
340
 
328
- promise[data.reject ? 'reject' : 'resolve'](data);
329
- delete me.promises[replyId]
341
+ if (data) {
342
+ promise[data.reject ? 'reject' : 'resolve'](data);
343
+ delete me.promises[replyId]
344
+ }
330
345
  }
331
346
  }
332
347
 
@@ -385,7 +400,14 @@ class Manager extends Base {
385
400
 
386
401
  return new Promise((resolve, reject) => {
387
402
  let message = me.sendMessage(dest, opts, transfer),
388
- msgId = message.id;
403
+ msgId;
404
+
405
+ if (!message) {
406
+ reject(new Error(me.stopCommunication ? 'Communication is stopped.' : `Target worker '${dest}' does not exist.`));
407
+ return
408
+ }
409
+
410
+ msgId = message.id;
389
411
 
390
412
  me.promises[msgId] = {reject, resolve}
391
413
  })
@@ -412,7 +434,7 @@ class Manager extends Base {
412
434
  * @param {Array} [transfer] An optional array of Transferable objects to transfer ownership of.
413
435
  * If the ownership of an object is transferred, it becomes unusable (neutered) in the context it was sent from
414
436
  * and becomes available only to the worker it was sent to.
415
- * @returns {Neo.worker.Message}
437
+ * @returns {Neo.worker.Message|null}
416
438
  * @protected
417
439
  */
418
440
  sendMessage(dest, opts, transfer) {
@@ -427,17 +449,53 @@ class Manager extends Base {
427
449
  worker = me.getWorker(dest)
428
450
  }
429
451
 
430
- if (!worker) {
431
- throw new Error('Called sendMessage for a worker that does not exist: ' + dest)
452
+ if (worker) {
453
+ opts.destination = dest;
454
+
455
+ message = new Message(opts);
456
+
457
+ (worker.port ? worker.port : worker).postMessage(message, transfer);
458
+ return message
432
459
  }
460
+ }
461
+
462
+ return null
463
+ }
464
+
465
+ /**
466
+ * Initiates a global Neo.config change from the Main Thread.
467
+ *
468
+ * This method acts as a proxy, routing the config change request to the App Worker.
469
+ * This design centralizes the complex multi-threaded and multi-window synchronization
470
+ * logic within the App Worker's `setGlobalConfig` method.
471
+ *
472
+ * Developers should typically use `Neo.setGlobalConfig(config)` directly,
473
+ * which will correctly resolve to this proxy when called from the Main Thread.
474
+ *
475
+ * @param {Object} config The partial or full Neo.config object with changes to apply.
476
+ */
477
+ setGlobalConfig(config) {
478
+ // Remotely calls the App Worker's setGlobalConfig method.
479
+ // This ensures all global config changes are processed through the App Worker
480
+ // which contains the centralized multi-window synchronization logic.
481
+ Neo.worker.App.setGlobalConfig(config)
482
+ }
433
483
 
434
- opts.destination = dest;
484
+ /**
485
+ * Change Neo.config globally from a worker
486
+ * @param {Object} data
487
+ * @param {Boolean} data.broadcast
488
+ * @param {Object} data.config
489
+ * @param {String} [data.excludeOrigin]
490
+ */
491
+ setNeoConfig({broadcast, config, excludeOrigin}) {
492
+ let me = this;
435
493
 
436
- message = new Message(opts);
494
+ Neo.merge(Neo.config, config);
437
495
 
438
- (worker.port ? worker.port : worker).postMessage(message, transfer);
439
- return message
440
- }
496
+ me.fire('neoConfigChange', config);
497
+
498
+ broadcast && me.broadcast({action: 'setNeoConfig', config}, excludeOrigin)
441
499
  }
442
500
  }
443
501