@genesislcap/grid-pro 14.480.0 → 14.481.1-alpha-69af5f8.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.
@@ -27,9 +27,32 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
27
27
  getRows(params) {
28
28
  return __awaiter(this, void 0, void 0, function* () {
29
29
  var _a, _b;
30
- // Use separated filtering and sorting setup
31
- yield this.setupFiltering(params);
32
- yield this.setupSorting(params);
30
+ // setupFiltering()/setupSorting() return true when they triggered refreshDatasource()
31
+ // internally (a filter/sort change). In that case reloadResourceData() has already
32
+ // re-initialized the underlying datasource and - only AFTER that init completed - purged
33
+ // AG Grid's SSRM cache, which makes AG Grid re-invoke getRows() with fresh params. This
34
+ // (now superseded) invocation must therefore stop here: continuing would race the re-init
35
+ // and attach a second subscription for stale params. That race was the root cause of two
36
+ // observed bugs: rows from the PREVIOUS criteria leaking into the new filter's result set,
37
+ // and broken loads when the stale call recreated the stream on a destroyed datasource.
38
+ //
39
+ // CRITICAL: the superseded request MUST still be resolved via params.fail() - never left
40
+ // dangling. AG Grid's RowNodeBlockLoader caps concurrent datasource requests
41
+ // (maxConcurrentDatasourceRequests, default 2) and only releases a slot when
42
+ // success()/fail() is invoked. A silent return leaks the slot FOREVER; after two
43
+ // filter/sort changes both slots are gone and the grid permanently stops issuing
44
+ // getRows() - no purge, scroll or new filter can ever revive it (observed as: clearing
45
+ // a filter leaves an empty grid, re-filtering does nothing). fail() here is safe: the
46
+ // purge already destroyed the cache this request belongs to, and LazyCache.onLoadFailed
47
+ // no-ops on a dead cache (guarded internally) - so the only effect is freeing the slot.
48
+ if (yield this.setupFiltering(params)) {
49
+ params.fail();
50
+ return;
51
+ }
52
+ if (yield this.setupSorting(params)) {
53
+ params.fail();
54
+ return;
55
+ }
33
56
  if (this.pagination && !this.isNewPageSize && this.currentSequenceId > 0) {
34
57
  const requestFilter = JSON.stringify((_a = params.request.filterModel) !== null && _a !== void 0 ? _a : {});
35
58
  const currentFilter = JSON.stringify((_b = this.currentFilterModel) !== null && _b !== void 0 ? _b : {});
@@ -41,7 +64,21 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
41
64
  if (!this.dataserverStream) {
42
65
  this.dataserverStream = yield this.createDataserverStreamFunc(this.resourceParams);
43
66
  }
67
+ const blockStartRow = Number.isFinite(Number(params.request.startRow))
68
+ ? Number(params.request.startRow)
69
+ : 0;
70
+ const blockEndRow = Number.isFinite(Number(params.request.endRow))
71
+ ? Number(params.request.endRow)
72
+ : blockStartRow + Number(this.maxRows);
73
+ // Infinite scroll keeps a session-cumulative row cache (see handleCurrentStreamLoad);
74
+ // when it already covers this block (AG Grid re-requesting a purged block, or an earlier
75
+ // over-delivery prefetched it), or the stream already ended, no further stream messages
76
+ // may ever arrive for this request - it must be resolved from the cache, synchronously.
77
+ const blockAlreadyCovered = !this.pagination &&
78
+ this.currentSequenceId >= 1 &&
79
+ (this.rowData.size >= blockEndRow || !this.moreRows);
44
80
  if (this.currentSequenceId >= 1 &&
81
+ !blockAlreadyCovered &&
45
82
  (this.moreRows || params.request.startRow >= Number(this.maxRows))) {
46
83
  if (this.pagination) {
47
84
  const startRow = Number.isFinite(Number(params.request.startRow))
@@ -58,16 +95,80 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
58
95
  this.connect.getMoreRows(this.sourceRef, viewNumber);
59
96
  }
60
97
  }
98
+ // NOTE: getRows() deliberately does NOT unsubscribe previous subscriptions here - this
99
+ // stream is tied to a live server-side feed (a DATA_LOGON session), and .unsubscribe()
100
+ // sends DATA_LOGOFF to the SERVER, killing the whole session (confirmed: every later
101
+ // block request failed outright after an experimental per-block unsubscribe). Earlier
102
+ // subscriptions go inert via their own applyResult flag once resolved.
61
103
  let applyResult = true;
104
+ const clearSafetyTimer = (timer) => {
105
+ if (timer !== undefined) {
106
+ clearTimeout(timer);
107
+ }
108
+ };
109
+ const resolveBlock = (timer) => {
110
+ if (!applyResult) {
111
+ return;
112
+ }
113
+ applyResult = false;
114
+ clearSafetyTimer(timer);
115
+ const allRows = Array.from(this.rowData.values());
116
+ const successRowData = {
117
+ // Non-pagination: deliver only THIS block's slice of the session-cumulative
118
+ // cache - AG Grid SSRM expects each request to receive exactly the rows for its
119
+ // own startRow..endRow range, not everything loaded so far.
120
+ rowData: this.pagination ? allRows : allRows.slice(blockStartRow, blockEndRow),
121
+ };
122
+ successRowData.rowCount = this.getCorrectRowCount(params);
123
+ this.lastSuccessRowData = successRowData;
124
+ params.success(successRowData);
125
+ this.notifyNoDataAvailableIfEmpty(params, successRowData);
126
+ };
127
+ if (blockAlreadyCovered) {
128
+ resolveBlock();
129
+ return;
130
+ }
131
+ // Safety valve: a block must never stay pending forever if the stream goes quiet without
132
+ // delivering a full batch (anomalous, but an eternally-spinning grid is the worst outcome).
133
+ // Resolving short with the honest accumulated count makes AG Grid end the dataset there.
134
+ const safetyTimer = setTimeout(() => {
135
+ if (applyResult) {
136
+ logger.warn(`SSRM block ${blockStartRow}-${blockEndRow} for ${this.resourceName} timed out waiting for a full batch; resolving with ${this.rowData.size} accumulated row(s).`);
137
+ resolveBlock(safetyTimer);
138
+ }
139
+ }, DataserverServerSideDatasource.BLOCK_RESOLVE_TIMEOUT_MS);
62
140
  this.dataserverStreamSubscription = this.dataserverStream.subscribe((dataserverResult) => {
63
141
  var _a;
64
- const incomingRowCount = (_a = dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.ROW) === null || _a === void 0 ? void 0 : _a.length;
65
- const shouldApply = applyResult || (incomingRowCount > 0 && this.rowData.size === 0);
66
- if (shouldApply) {
142
+ if (!applyResult) {
143
+ // This request already resolved - later messages belong to the next block's
144
+ // subscription, which processes them itself.
145
+ return;
146
+ }
147
+ const messageType = dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.MESSAGE_TYPE;
148
+ if (messageType &&
149
+ (messageType === MessageType.LOGOFF_ACK || messageType === MessageType.MSG_NACK)) {
150
+ applyResult = false;
151
+ clearSafetyTimer(safetyTimer);
152
+ if (this.errorHandlerFunc) {
153
+ const errorMessage = messageType === MessageType.LOGOFF_ACK
154
+ ? `Connection lost to ${this.resourceName}`
155
+ : `Authentication failed for ${this.resourceName}`;
156
+ this.errorHandlerFunc(errorMessage, 'connection');
157
+ }
158
+ params.fail();
159
+ return;
160
+ }
161
+ if (this.pagination) {
162
+ // Original vendor behaviour for paginated mode: each message resolves the
163
+ // request (page swap), including the corrective snapshot() workaround for GSF's
164
+ // criteria-blind ROWS_COUNT.
67
165
  DOM.queueUpdate(() => __awaiter(this, void 0, void 0, function* () {
68
166
  var _a, _b;
167
+ if (!applyResult) {
168
+ return;
169
+ }
69
170
  // TODO: this is a bit of hack, due GSF not returning a different ROWS_COUNT when there is a CRITERIA_MATCH
70
- if (this.resourceParams.CRITERIA_MATCH && this.pagination) {
171
+ if (this.resourceParams.CRITERIA_MATCH && !this.criteriaRowCountResolved) {
71
172
  const updatedInfo = yield this.connect.snapshot(this.resourceName, {
72
173
  MAX_ROWS: this.maxView,
73
174
  MAX_VIEW: this.maxView,
@@ -79,14 +180,55 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
79
180
  this.clientRowsCount = 0;
80
181
  }
81
182
  else {
82
- this.serverRowsCount = this.resourceParams.CRITERIA_MATCH
83
- ? ((_b = (_a = updatedInfo.ROW) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : updatedInfo.ROWS_COUNT)
84
- : updatedInfo.ROWS_COUNT;
183
+ // Neither field alone is trustworthy: GSF's ROWS_COUNT ignores
184
+ // CRITERIA_MATCH (full unfiltered total), while ROW.length is capped at the
185
+ // server's page size. Math.max can over-report (safe/self-correcting once
186
+ // the stream signals no more rows) but never under-reports - the dangerous
187
+ // direction, as AG Grid stops requesting blocks once the count is reached.
188
+ const rowsCount = (_a = updatedInfo.ROWS_COUNT) !== null && _a !== void 0 ? _a : 0;
189
+ const rowLength = (_b = updatedInfo.ROW) === null || _b === void 0 ? void 0 : _b.length;
190
+ this.serverRowsCount =
191
+ rowLength !== undefined ? Math.max(rowsCount, rowLength) : rowsCount;
85
192
  }
193
+ this.criteriaRowCountResolved = true;
86
194
  }
195
+ clearSafetyTimer(safetyTimer);
87
196
  this.applyServerSideData(params, dataserverResult);
88
197
  applyResult = false;
89
198
  }));
199
+ return;
200
+ }
201
+ // Infinite scroll: GSF delivers one MAX_ROWS batch split across SEVERAL stream
202
+ // messages when a selective CRITERIA_MATCH makes matches sparse (observed on a live
203
+ // trace: SEQUENCE_IDs 1-7 carrying 14+17+7+12+22+25+3 = exactly 100 = MAX_ROWS rows).
204
+ // Resolving on the first message - the previous behaviour - handed AG Grid a fraction
205
+ // of the block and dropped the rest, which rendered as "ERR" stub rows. Instead,
206
+ // accumulate every message into the session cache and resolve only once this block's
207
+ // full extent is covered or the dataset genuinely ended.
208
+ if (dataserverResult && dataserverResult.MORE_ROWS !== undefined) {
209
+ this.moreRows = dataserverResult.MORE_ROWS;
210
+ }
211
+ if (dataserverResult && dataserverResult.ROW) {
212
+ const nextMessage = dataServerResultFilter(dataserverResult);
213
+ this.handleCurrentStreamLoad(nextMessage);
214
+ this.currentSequenceId = dataserverResult.SEQUENCE_ID;
215
+ if (this.currentSequenceId === 1) {
216
+ this.sourceRef = dataserverResult.SOURCE_REF;
217
+ if (this.serverRowsCount === 0) {
218
+ this.serverRowsCount = (_a = dataserverResult.ROWS_COUNT) !== null && _a !== void 0 ? _a : this.rowData.size;
219
+ }
220
+ }
221
+ }
222
+ else if (this.currentSequenceId === 0 && !this.moreRows) {
223
+ // The session's very first response carries no rows at all - the criteria
224
+ // matches nothing; resolve as empty instead of waiting for the timeout.
225
+ applyResult = false;
226
+ clearSafetyTimer(safetyTimer);
227
+ this.completeEmptyResult(params);
228
+ return;
229
+ }
230
+ if (this.rowData.size >= blockEndRow || !this.moreRows) {
231
+ DOM.queueUpdate(() => resolveBlock(safetyTimer));
90
232
  }
91
233
  });
92
234
  });
@@ -134,10 +276,11 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
134
276
  if (this.serverRowsCount === 0) {
135
277
  this.serverRowsCount = (_a = result.ROWS_COUNT) !== null && _a !== void 0 ? _a : this.rowData.size;
136
278
  }
137
- if (this.getResourceParam('CRITERIA_MATCH') && result.ROW) {
138
- this.serverRowsCount = result.ROW.length;
139
- this.clientRowsCount = result.ROW.length;
140
- }
279
+ // NOTE: deliberately NOT overriding serverRowsCount with result.ROW.length under
280
+ // CRITERIA_MATCH here (previous behaviour). ROW.length is just the first batch's size,
281
+ // not the total match count - the override clobbered the corrected count resolved by
282
+ // the snapshot() workaround in getRows() and made AG Grid stop requesting further
283
+ // blocks after the first one.
141
284
  }
142
285
  const successRowData = {
143
286
  rowData: Array.from(this.rowData.values()),
@@ -163,31 +306,39 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
163
306
  this.resourceParams.ORDER_BY = null;
164
307
  this.resourceParams.REVERSE = null;
165
308
  yield this.refreshDatasource(params);
166
- return;
309
+ // true = this request is superseded; the caller (getRows) must stop - see the
310
+ // matching contract on setupFiltering() in server-side.resource-base.ts.
311
+ return true;
167
312
  }
168
313
  else if (toBeAppliedSortModel.length > 0) {
169
314
  const coldIdBeingSorted = toBeAppliedSortModel[0].colId; // Not allowing multiple sorts by user
170
315
  const sortTypeBeingApplied = toBeAppliedSortModel[0].sort;
171
316
  const orderByAndToBeSortedColIds = this.getOrderByAndToBeSortedColIds(this.resourceIndexes, coldIdBeingSorted);
172
317
  if (!orderByAndToBeSortedColIds) {
173
- this.calculatedRowsCount = 0;
174
318
  const availableIndexes = getAvailableIndexes(this.resourceIndexes);
175
319
  const availableIndexFields = getAvailableIndexFields(this.resourceIndexes);
176
- logger.warn(`The FIELD/column (${coldIdBeingSorted}) being sorted is not part of an INDEX, required for the [orderBy] operation. See https://learn.genesis.global/docs/database/data-types/index-entities/`);
320
+ logger.warn(`The FIELD/column (${coldIdBeingSorted}) being sorted is not part of an INDEX, required for the [orderBy] operation - ignoring this sort and loading unsorted data. See https://learn.genesis.global/docs/database/data-types/index-entities/`);
177
321
  logger.debug('Available indexes:', availableIndexes);
178
322
  logger.debug('Columns that can be sorted with the available indexes:', availableIndexFields);
179
- params.fail();
180
- return;
323
+ // Ignore the un-sortable sort instead of failing the request. Failing here killed
324
+ // the ENTIRE first load (single "ERR" row, nothing ever loads) when a RESTORED
325
+ // grid workspace/settings carried a sort on a non-indexed column - the user had no
326
+ // way to recover except clearing the sort manually. Remember the model so
327
+ // identical follow-up requests don't re-warn, leave ORDER_BY/REVERSE untouched,
328
+ // and let getRows() carry on unsorted.
329
+ this.currentSortModel = toBeAppliedSortModel;
330
+ return false;
181
331
  }
182
332
  else if (JSON.stringify(toBeAppliedSortModel) !== JSON.stringify(this.currentSortModel)) {
183
333
  this.currentSortModel = toBeAppliedSortModel;
184
334
  this.resourceParams.ORDER_BY = orderByAndToBeSortedColIds.orderBy;
185
335
  this.resourceParams.REVERSE = sortTypeBeingApplied === 'desc' ? true : false;
186
336
  yield this.refreshDatasource(params);
187
- return;
337
+ return true;
188
338
  }
189
339
  }
190
340
  }
341
+ return false;
191
342
  });
192
343
  }
193
344
  destroy() {
@@ -203,14 +354,45 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
203
354
  });
204
355
  }
205
356
  handleCurrentStreamLoad(result) {
206
- var _a, _b;
357
+ var _a, _b, _c, _d, _e;
207
358
  if (!result)
208
359
  return;
209
- const rows = new Map();
360
+ // Pagination swaps whole pages, so each message replaces the buffer (original behaviour).
361
+ // Infinite scroll instead accumulates a session-cumulative cache: GSF splits one MAX_ROWS
362
+ // batch across several incremental messages when a CRITERIA_MATCH filter is selective, and
363
+ // later batches append after earlier ones - replacing the buffer on every message threw
364
+ // away all but the latest fragment (see the accumulation comment in getRows()). Maps
365
+ // preserve insertion order, so the cache stays in server delivery order and can be sliced
366
+ // by block range.
367
+ const rows = this.pagination ? new Map() : new Map(this.rowData);
210
368
  (_a = result.inserts) === null || _a === void 0 ? void 0 : _a.forEach((insertData) => {
211
369
  rows.set(insertData[this.rowId], insertData);
212
370
  });
213
- this.clientRowsCount += (_b = result.inserts.length) !== null && _b !== void 0 ? _b : 0;
371
+ // Live QUERY_UPDATE messages can arrive on this stream while a block is pending: MODIFY
372
+ // rows may be partial (only changed fields), so merge them into the cached row rather than
373
+ // clobbering it; DELETE rows drop out of the cache. (Previously an update/delete-only
374
+ // message replaced the whole buffer with an EMPTY map, wiping every loaded row.)
375
+ (_b = result.updates) === null || _b === void 0 ? void 0 : _b.forEach((updateData) => {
376
+ const key = updateData === null || updateData === void 0 ? void 0 : updateData[this.rowId];
377
+ if (key === undefined) {
378
+ return;
379
+ }
380
+ const existing = rows.get(key);
381
+ rows.set(key, existing ? Object.assign(Object.assign({}, existing), updateData) : updateData);
382
+ });
383
+ (_c = result.deletes) === null || _c === void 0 ? void 0 : _c.forEach((deleteData) => {
384
+ const key = deleteData === null || deleteData === void 0 ? void 0 : deleteData[this.rowId];
385
+ if (key !== undefined) {
386
+ rows.delete(key);
387
+ }
388
+ });
389
+ this.clientRowsCount += (_e = (_d = result.inserts) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
214
390
  this.rowData = rows;
215
391
  }
216
392
  }
393
+ /**
394
+ * How long a non-paginated SSRM block may stay pending before it is force-resolved with
395
+ * whatever rows have accumulated so far. See the safety valve in getRows().
396
+ * @internal
397
+ */
398
+ DataserverServerSideDatasource.BLOCK_RESOLVE_TIMEOUT_MS = 10000;
@@ -255,7 +255,7 @@ export class ReqRepServerSideDatasource extends BaseServerSideDatasource {
255
255
  this.currentSortModel = null;
256
256
  this.resourceParams.DETAILS.ORDER_BY = undefined;
257
257
  yield this.refreshDatasource(params);
258
- return;
258
+ return true;
259
259
  }
260
260
  else if (toBeAppliedSortModel.length > 0) {
261
261
  const coldIdBeingSorted = toBeAppliedSortModel[0].colId; // Not allowing multiple sorts by user
@@ -266,10 +266,11 @@ export class ReqRepServerSideDatasource extends BaseServerSideDatasource {
266
266
  this.resourceParams.DETAILS.ORDER_BY =
267
267
  coldIdBeingSorted + (sortTypeBeingApplied === 'desc' ? ' DESC' : ' ASC'); // Use the column directly
268
268
  yield this.refreshDatasource(params);
269
- return;
269
+ return true;
270
270
  }
271
271
  }
272
272
  }
273
+ return false;
273
274
  });
274
275
  }
275
276
  refreshDatasource(params) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/grid-pro",
3
3
  "description": "Genesis Foundation AG Grid",
4
- "version": "14.480.0",
4
+ "version": "14.481.1-alpha-69af5f8.0",
5
5
  "sideEffects": false,
6
6
  "license": "SEE LICENSE IN license.txt",
7
7
  "main": "dist/esm/index.js",
@@ -40,20 +40,20 @@
40
40
  }
41
41
  },
42
42
  "devDependencies": {
43
- "@genesislcap/foundation-testing": "14.480.0",
44
- "@genesislcap/genx": "14.480.0",
45
- "@genesislcap/rollup-builder": "14.480.0",
46
- "@genesislcap/ts-builder": "14.480.0",
47
- "@genesislcap/uvu-playwright-builder": "14.480.0",
48
- "@genesislcap/vite-builder": "14.480.0",
49
- "@genesislcap/webpack-builder": "14.480.0"
43
+ "@genesislcap/foundation-testing": "14.481.1-alpha-69af5f8.0",
44
+ "@genesislcap/genx": "14.481.1-alpha-69af5f8.0",
45
+ "@genesislcap/rollup-builder": "14.481.1-alpha-69af5f8.0",
46
+ "@genesislcap/ts-builder": "14.481.1-alpha-69af5f8.0",
47
+ "@genesislcap/uvu-playwright-builder": "14.481.1-alpha-69af5f8.0",
48
+ "@genesislcap/vite-builder": "14.481.1-alpha-69af5f8.0",
49
+ "@genesislcap/webpack-builder": "14.481.1-alpha-69af5f8.0"
50
50
  },
51
51
  "dependencies": {
52
- "@genesislcap/foundation-comms": "14.480.0",
53
- "@genesislcap/foundation-criteria": "14.480.0",
54
- "@genesislcap/foundation-logger": "14.480.0",
55
- "@genesislcap/foundation-ui": "14.480.0",
56
- "@genesislcap/foundation-utils": "14.480.0",
52
+ "@genesislcap/foundation-comms": "14.481.1-alpha-69af5f8.0",
53
+ "@genesislcap/foundation-criteria": "14.481.1-alpha-69af5f8.0",
54
+ "@genesislcap/foundation-logger": "14.481.1-alpha-69af5f8.0",
55
+ "@genesislcap/foundation-ui": "14.481.1-alpha-69af5f8.0",
56
+ "@genesislcap/foundation-utils": "14.481.1-alpha-69af5f8.0",
57
57
  "@microsoft/fast-colors": "5.3.1",
58
58
  "@microsoft/fast-components": "2.30.6",
59
59
  "@microsoft/fast-element": "1.14.0",
@@ -91,5 +91,5 @@
91
91
  "require": "./dist/react.cjs"
92
92
  }
93
93
  },
94
- "gitHead": "6d2bc60f9700ab9f5a6d8c1834baebbd66ae5e6b"
94
+ "gitHead": "ace4f24de5b1fabb4c07b24fb601a5b797ac6876"
95
95
  }