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.
- package/.github/RELEASE_NOTES/v10.3.5md +4 -0
- package/.github/RELEASE_NOTES/v10.4.0.md +24 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/buildScripts/buildAll.mjs +17 -15
- package/examples/grid/bigData/ControlsContainer.mjs +1 -3
- package/examples/grid/bigData/MainStore.mjs +11 -2
- package/learn/guides/datahandling/Grids.md +7 -7
- package/package.json +4 -4
- package/src/DefaultConfig.mjs +2 -2
- package/src/collection/Base.mjs +15 -6
- package/src/data/Store.mjs +25 -0
- package/src/grid/Body.mjs +25 -0
- package/src/grid/ScrollManager.mjs +23 -11
|
@@ -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
package/apps/portal/index.html
CHANGED
|
@@ -150,23 +150,25 @@ if (programOpts.info) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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);
|
|
@@ -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.
|
|
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.
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
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.
|
|
102
|
+
"sass": "^1.90.0",
|
|
103
103
|
"siesta-lite": "5.5.2",
|
|
104
104
|
"terser": "^5.43.1",
|
|
105
105
|
"url": "^0.11.4",
|
package/src/DefaultConfig.mjs
CHANGED
|
@@ -299,12 +299,12 @@ const DefaultConfig = {
|
|
|
299
299
|
useVdomWorker: true,
|
|
300
300
|
/**
|
|
301
301
|
* buildScripts/injectPackageVersion.mjs will update this value
|
|
302
|
-
* @default '10.
|
|
302
|
+
* @default '10.4.0'
|
|
303
303
|
* @memberOf! module:Neo
|
|
304
304
|
* @name config.version
|
|
305
305
|
* @type String
|
|
306
306
|
*/
|
|
307
|
-
version: '10.
|
|
307
|
+
version: '10.4.0'
|
|
308
308
|
};
|
|
309
309
|
|
|
310
310
|
Object.assign(DefaultConfig, {
|
package/src/collection/Base.mjs
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/src/data/Store.mjs
CHANGED
|
@@ -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
|
|