neo.mjs 10.3.4 → 10.4.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.
@@ -0,0 +1,4 @@
1
+ # Neo.mjs v10.3.4 Release Notes
2
+
3
+ This patch release enhances the build-all script to only copy generated docs output into dist envs, in case the
4
+ docs app does exist (might not be the case inside custom workspaces).
@@ -0,0 +1,24 @@
1
+ # Neo.mjs v10.4.0 Release Notes
2
+
3
+ ## Highlights
4
+
5
+ This release focuses heavily on performance and robustness, especially when dealing with very large datasets in collections, stores, and grids. Key improvements include non-blocking chunked data loading for stores, a fix for stack overflow errors when adding large arrays to collections, and significant optimizations for grid scrolling and data clearing.
6
+
7
+ ## Enhancements
8
+
9
+ ### 1. High-Performance Chunking for `Store.add()`
10
+ To prevent the UI from freezing when adding massive datasets (e.g., 1M+ records), `Store.add()` now uses a two-chunk loading strategy. It first adds a small "interactive" batch of records to make the UI responsive almost instantly. It then silently adds the remaining data in the background by leveraging the `suspendEvents` flag, and fires a final `load` event to update the UI (e.g., grid scrollbars) once complete. This ensures a non-blocking, highly responsive user experience.
11
+
12
+ ### 2. Refactored Grid ScrollManager for Smoother Scrolling
13
+ The `grid.ScrollManager` has been refactored to use the framework's built-in `delayable` system instead of manual `setTimeout` logic. Both vertical and horizontal scroll event handlers are now throttled to align with a 60fps refresh rate, and a debounced `onBodyScrollEnd` handler efficiently manages the `isScrolling` state. This results in smoother scrolling, more consistent behavior, and cleaner, more maintainable code.
14
+
15
+ ### 3. Grid `onStoreLoad` Fast Path for Instant Clearing
16
+ A performance "fast path" has been added to the `grid.Body` for scenarios where a store is cleared. Instead of performing a full VDOM diff between the existing rows and an empty dataset, the grid now detects this specific case and directly clears the VDOM and the real DOM. This reduces the time to clear a large grid from ~13ms to a near-instantaneous operation.
17
+
18
+ ## Bug Fixes
19
+
20
+ ### 1. Fixed `collection.Base#splice` Stack Overflow
21
+ A `RangeError: Maximum call stack size exceeded` would occur when adding a very large number of items (e.g., >100k) to an already populated collection at runtime. This was caused by using the spread operator (`...`) on a massive array. The `splice` method now intelligently switches to a safer `concat()`-based approach for large arrays, preventing the crash while retaining the high performance of native `splice` for smaller arrays.
22
+
23
+ ### 2. Fixed `collection.Base#construct` Race Condition
24
+ A race condition was fixed where methods on a collection could be called during instantiation before the collection's internal properties were initialized. The `construct` method's logic was reordered to ensure all internal properties are defined *before* the parent constructor is called, making the class more robust and preventing subtle bugs during component initialization.
package/ServiceWorker.mjs CHANGED
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='10.3.4'
23
+ * @member {String} version='10.4.0'
24
24
  */
25
- version: '10.3.4'
25
+ version: '10.4.0'
26
26
  }
27
27
 
28
28
  /**
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-08-09",
19
+ "datePublished": "2025-08-11",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -108,7 +108,7 @@ class FooterContainer extends Container {
108
108
  }, {
109
109
  module: Component,
110
110
  cls : ['neo-version'],
111
- text : 'v10.3.4'
111
+ text : 'v10.4.0'
112
112
  }]
113
113
  }],
114
114
  /**
@@ -150,23 +150,25 @@ if (programOpts.info) {
150
150
  }
151
151
  }
152
152
 
153
- if (parsedocs === 'yes' && fs.existsSync(path.join(cwd, 'docs/app'))) {
154
- childProcess = spawnSync(npmCmd, ['run', 'generate-docs-json'], cpOpts);
155
- childProcess.status && process.exit(childProcess.status);
156
- }
153
+ if (fs.existsSync(path.join(cwd, 'docs/app'))) {
154
+ if (parsedocs === 'yes') {
155
+ childProcess = spawnSync(npmCmd, ['run', 'generate-docs-json'], cpOpts);
156
+ childProcess.status && process.exit(childProcess.status);
157
+ }
157
158
 
158
- if (parsedocs === 'yes' && (env === 'all' || env === 'dev')) {
159
- childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/output') } -t ${path.resolve(cwd, 'dist/development/docs/output')}`], cpOpts);
160
- childProcess.status && process.exit(childProcess.status);
161
- childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/resources')} -t ${path.resolve(cwd, 'dist/development/docs/resources')}`], cpOpts);
162
- childProcess.status && process.exit(childProcess.status);
163
- }
159
+ if (parsedocs === 'yes' && (env === 'all' || env === 'dev')) {
160
+ childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/output') } -t ${path.resolve(cwd, 'dist/development/docs/output')}`], cpOpts);
161
+ childProcess.status && process.exit(childProcess.status);
162
+ childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/resources')} -t ${path.resolve(cwd, 'dist/development/docs/resources')}`], cpOpts);
163
+ childProcess.status && process.exit(childProcess.status);
164
+ }
164
165
 
165
- if (parsedocs === 'yes' && (env === 'all' || env === 'prod')) {
166
- childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/output') } -t ${path.resolve(cwd, 'dist/production/docs/output')}`], cpOpts);
167
- childProcess.status && process.exit(childProcess.status);
168
- childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/resources')} -t ${path.resolve(cwd, 'dist/production/docs/resources')}`], cpOpts);
169
- childProcess.status && process.exit(childProcess.status);
166
+ if (parsedocs === 'yes' && (env === 'all' || env === 'prod')) {
167
+ childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/output') } -t ${path.resolve(cwd, 'dist/production/docs/output')}`], cpOpts);
168
+ childProcess.status && process.exit(childProcess.status);
169
+ childProcess = spawnSync('node', [`${neoPath}/buildScripts/copyFolder.mjs -s ${path.resolve(cwd, 'docs/resources')} -t ${path.resolve(cwd, 'dist/production/docs/resources')}`], cpOpts);
170
+ childProcess.status && process.exit(childProcess.status);
171
+ }
170
172
  }
171
173
 
172
174
  const processTime = (Math.round((new Date - startDate) * 100) / 100000).toFixed(2);
@@ -264,9 +264,7 @@ class ControlsContainer extends Container {
264
264
  filter: me.updateRowsLabel,
265
265
  load : me.updateRowsLabel,
266
266
  scope : me
267
- });
268
-
269
- store.getCount() > 0 && me.updateRowsLabel()
267
+ })
270
268
  }
271
269
 
272
270
  /**
@@ -24,6 +24,7 @@ class MainStore extends Store {
24
24
  amountRows_: 1000,
25
25
  /**
26
26
  * @member {Object[]} filters
27
+ * @reactive
27
28
  */
28
29
  filters: [{
29
30
  property: 'firstname',
@@ -75,7 +76,11 @@ class MainStore extends Store {
75
76
 
76
77
  console.log('Start creating records');
77
78
 
78
- me.data = data;
79
+ if (me.items?.length > 0) {
80
+ me.clear()
81
+ }
82
+
83
+ me.add(data);
79
84
 
80
85
  console.log(`Record creation total time: ${Math.round(performance.now() - start)}ms`)
81
86
  }
@@ -94,7 +99,11 @@ class MainStore extends Store {
94
99
 
95
100
  console.log('Start creating records');
96
101
 
97
- me.data = data;
102
+ if (me.items?.length > 0) {
103
+ me.clear()
104
+ }
105
+
106
+ me.add(data);
98
107
 
99
108
  console.log(`Record creation total time: ${Math.round(performance.now() - start)}ms`)
100
109
  }
@@ -53,7 +53,7 @@ MainView = Neo.setupClass(MainView);
53
53
 
54
54
  In this example, we create a simple grid with two columns. The `dataField` in each column configuration maps to a
55
55
  field in the store's model. You can further customize the grid's behavior and appearance
56
- by providing `bodyConfig` and `headerToolbarConfig`.
56
+ by providing `body` and `headerToolbarConfig`.
57
57
 
58
58
  ## Integrating with Stores
59
59
 
@@ -556,11 +556,11 @@ const myGrid = Neo.create(GridContainer, {
556
556
 
557
557
  ### Animated Row Sorting
558
558
 
559
- To animate row sorting, set the `animatedRowSorting` config to `true` on the `Neo.grid.Body` (via `bodyConfig`).
559
+ To animate row sorting, set the `animatedRowSorting` config to `true` on the `Neo.grid.Body` (via `body`).
560
560
 
561
561
  ```javascript readonly
562
562
  const myGrid = Neo.create(GridContainer, {
563
- bodyConfig: {
563
+ body: {
564
564
  animatedRowSorting: true
565
565
  },
566
566
  // ...
@@ -569,7 +569,7 @@ const myGrid = Neo.create(GridContainer, {
569
569
 
570
570
  ## Selection Models
571
571
 
572
- The grid's selection behavior is controlled by a selection model, which you can configure on the `bodyConfig`.
572
+ The grid's selection behavior is controlled by a selection model, which you can configure on the `body`.
573
573
 
574
574
  Available selection models in `Neo.selection.grid`:
575
575
  - `RowModel`: Selects entire rows.
@@ -582,7 +582,7 @@ import {RowModel} from '../../../src/selection/grid/_export.mjs';
582
582
 
583
583
  const myGrid = Neo.create(GridContainer, {
584
584
  // ...
585
- bodyConfig: {
585
+ body: {
586
586
  selectionModel: RowModel
587
587
  }
588
588
  });
@@ -597,12 +597,12 @@ reducing memory consumption and improving rendering speed.
597
597
  You### Optimizing Virtual Rendering
598
598
 
599
599
  You can fine-tune the virtual rendering behavior with the `bufferRowRange` and `bufferColumnRange` configs in the
600
- `bodyConfig`. These settings define how many extra rows and columns to render outside the visible area to provide a
600
+ `body`. These settings define how many extra rows and columns to render outside the visible area to provide a
601
601
  smoother scrolling experience.
602
602
 
603
603
  ```javascript readonly
604
604
  const myGrid = Neo.create(GridContainer, {
605
- bodyConfig: {
605
+ body: {
606
606
  bufferRowRange : 5, // Render 5 extra rows above and below the visible area
607
607
  bufferColumnRange: 2 // Render 2 extra columns to the left and right of the visible area
608
608
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "10.3.4",
3
+ "version": "10.4.0",
4
4
  "description": "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -89,17 +89,17 @@
89
89
  "cssnano": "^7.1.0",
90
90
  "envinfo": "^7.14.0",
91
91
  "esbuild": "^0.25.8",
92
- "fs-extra": "^11.3.0",
92
+ "fs-extra": "^11.3.1",
93
93
  "highlightjs-line-numbers.js": "^2.9.0",
94
94
  "html-minifier-terser": "^7.2.0",
95
- "inquirer": "^12.9.0",
95
+ "inquirer": "^12.9.1",
96
96
  "marked": "^16.1.2",
97
97
  "monaco-editor": "0.50.0",
98
98
  "neo-jsdoc": "1.0.1",
99
99
  "neo-jsdoc-x": "1.0.5",
100
100
  "parse5": "^8.0.0",
101
101
  "postcss": "^8.5.6",
102
- "sass": "^1.89.2",
102
+ "sass": "^1.90.0",
103
103
  "siesta-lite": "5.5.2",
104
104
  "terser": "^5.43.1",
105
105
  "url": "^0.11.4",
@@ -299,12 +299,12 @@ const DefaultConfig = {
299
299
  useVdomWorker: true,
300
300
  /**
301
301
  * buildScripts/injectPackageVersion.mjs will update this value
302
- * @default '10.3.4'
302
+ * @default '10.4.0'
303
303
  * @memberOf! module:Neo
304
304
  * @name config.version
305
305
  * @type String
306
306
  */
307
- version: '10.3.4'
307
+ version: '10.4.0'
308
308
  };
309
309
 
310
310
  Object.assign(DefaultConfig, {
@@ -120,13 +120,9 @@ class Collection extends Base {
120
120
  * @param config
121
121
  */
122
122
  construct(config) {
123
- super.construct(config);
124
-
125
123
  let me = this,
126
124
  symbolConfig = {enumerable: false, writable: true};
127
125
 
128
- me.items = me.items || [];
129
-
130
126
  Object.defineProperties(me, {
131
127
  [countMutations] : {...symbolConfig, value: false},
132
128
  [isFiltered] : {...symbolConfig, value: false},
@@ -137,6 +133,10 @@ class Collection extends Base {
137
133
  [updatingIndex] : {...symbolConfig, value: 0}
138
134
  });
139
135
 
136
+ super.construct(config);
137
+
138
+ me.items = me.items || [];
139
+
140
140
  if (me.autoSort && me._sorters.length > 0) {
141
141
  me.doSort()
142
142
  }
@@ -1237,11 +1237,20 @@ class Collection extends Base {
1237
1237
  }
1238
1238
 
1239
1239
  if (addedItems.length > 0) {
1240
- if (items.length === 0) {
1240
+ if (!items || items.length === 0) {
1241
1241
  // Performance improvement for Safari, see: https://github.com/neomjs/neo/issues/6228
1242
1242
  me._items = addedItems
1243
1243
  } else {
1244
- items.splice(Neo.isNumber(index) ? index : items.length, 0, ...addedItems)
1244
+ const finalIndex = Neo.isNumber(index) ? index : items.length;
1245
+
1246
+ if (addedItems.length > 5000) {
1247
+ // Manually splice for large arrays to avoid a stack overflow
1248
+ const beginning = items.slice(0, finalIndex);
1249
+ const end = items.slice(finalIndex);
1250
+ me._items = beginning.concat(addedItems, end);
1251
+ } else {
1252
+ items.splice(finalIndex, 0, ...addedItems)
1253
+ }
1245
1254
  }
1246
1255
 
1247
1256
  if (me.autoSort && me._sorters.length > 0) {
@@ -139,6 +139,31 @@ class Store extends Base {
139
139
  * @returns {Number} the collection count
140
140
  */
141
141
  add(item) {
142
+ let items = Array.isArray(item) ? item : [item];
143
+ const threshold = 1000;
144
+
145
+ if (items.length > threshold) {
146
+ const me = this,
147
+ chunk = items.splice(0, threshold);
148
+
149
+ // 1. Add the first chunk. This fires 'mutate' -> 'load' and triggers the initial grid render.
150
+ super.add(me.createRecord(chunk));
151
+
152
+ // 2. Suspend events to prevent the next 'add' from firing 'load'.
153
+ me.suspendEvents = true;
154
+
155
+ // 3. Add the rest of the items silently.
156
+ super.add(me.createRecord(items));
157
+
158
+ // 4. Resume events.
159
+ me.suspendEvents = false;
160
+
161
+ // 5. Manually fire a final 'load' event to update the grid's scrollbar.
162
+ me.fire('load', me.items);
163
+
164
+ return me.count
165
+ }
166
+
142
167
  return super.add(this.createRecord(item))
143
168
  }
144
169
 
package/src/grid/Body.mjs CHANGED
@@ -938,6 +938,31 @@ class GridBody extends Component {
938
938
  onStoreLoad(data) {
939
939
  let me = this;
940
940
 
941
+ /*
942
+ * Fast path to handle clearing all rows (e.g., store.removeAll()).
943
+ * A full vdom diff against all existing rows is a performance bottleneck.
944
+ * This logic bypasses the standard update() cycle by directly clearing the vdom,
945
+ * vnode cache and the real DOM via textContent.
946
+ */
947
+ if (data?.length < 1) {
948
+ const vdomRoot = me.getVdomRoot();
949
+
950
+ // No change, opt out
951
+ if (vdomRoot.cn.length < 1) {
952
+ return
953
+ }
954
+
955
+ vdomRoot.cn = [];
956
+ me.getVnodeRoot().childNodes = [];
957
+
958
+ Neo.applyDeltas(me.appName, {
959
+ id : vdomRoot.id,
960
+ textContent: ''
961
+ });
962
+
963
+ return
964
+ }
965
+
941
966
  me.createViewData();
942
967
 
943
968
  if (me.mounted) {
@@ -5,6 +5,17 @@ import Base from '../core/Base.mjs';
5
5
  * @extends Neo.core.Base
6
6
  */
7
7
  class ScrollManager extends Base {
8
+ /**
9
+ * @member {Object} delayable
10
+ * @protected
11
+ * @static
12
+ */
13
+ static delayable = {
14
+ onBodyScroll : {type: 'throttle', timer: 16},
15
+ onBodyScrollEnd : {type: 'buffer', timer: 150},
16
+ onContainerScroll: {type: 'throttle', timer: 16}
17
+ }
18
+
8
19
  static config = {
9
20
  /**
10
21
  * @member {String} className='Neo.grid.ScrollManager'
@@ -47,11 +58,6 @@ class ScrollManager extends Base {
47
58
  * @protected
48
59
  */
49
60
  lastTouchY = 0
50
- /**
51
- * @member {Number|null}} scrollTimeoutId=null
52
- * @protected
53
- */
54
- scrollTimeoutId = null
55
61
  /**
56
62
  * Flag for identifying the ownership of a touchmove operation
57
63
  * @member {'body'|'container'|null} touchMoveOwner=null
@@ -92,14 +98,10 @@ class ScrollManager extends Base {
92
98
 
93
99
  me.scrollTop = scrollTop;
94
100
 
95
- me.scrollTimeoutId && clearTimeout(me.scrollTimeoutId);
96
-
97
- me.scrollTimeoutId = setTimeout(() => {
98
- body.isScrolling = false
99
- }, 100);
100
-
101
101
  body.set({isScrolling: true, scrollTop});
102
102
 
103
+ me.onBodyScrollEnd();
104
+
103
105
  if (touches) {
104
106
  if (me.touchMoveOwner !== 'container') {
105
107
  me.touchMoveOwner = 'body'
@@ -120,6 +122,13 @@ class ScrollManager extends Base {
120
122
  }
121
123
  }
122
124
 
125
+ /**
126
+ * @protected
127
+ */
128
+ onBodyScrollEnd() {
129
+ this.gridBody.isScrolling = false
130
+ }
131
+
123
132
  /**
124
133
  * @param {Object} data
125
134
  * @param {Number} data.scrollLeft
@@ -133,6 +142,9 @@ class ScrollManager extends Base {
133
142
 
134
143
  // We must ignore events for grid-scrollbar
135
144
  if (target.id.includes('grid-container')) {
145
+ body.isScrolling = true;
146
+ me.onBodyScrollEnd();
147
+
136
148
  me .scrollLeft = scrollLeft;
137
149
  body.scrollLeft = scrollLeft;
138
150