@genesislcap/grid-pro 15.50.0 → 15.50.1

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.
@@ -1,7 +1,7 @@
1
1
  import { __awaiter } from "tslib";
2
- import { MessageType, } from '@genesislcap/foundation-comms';
2
+ import { dataserverPagingStatus, describeDataserverPagingStop, MessageType, resolveMaxView, } from '@genesislcap/foundation-comms';
3
3
  import { logger } from '../utils';
4
- import { filterDataserverResult } from './dataserver-result.filter';
4
+ import { cachedRowKeyResolver, filterDataserverResult } from './dataserver-result.filter';
5
5
  import { DeliveredBlockLedger } from './delivered-block.ledger';
6
6
  import { convertFilterModelToCriteria } from './filter.utils';
7
7
  /**
@@ -115,16 +115,12 @@ export class GenesisInfiniteDatasource {
115
115
  this.rowData = new Map();
116
116
  /** Blocks waiting for the cache to reach their range. @internal */
117
117
  this.pendingBlocks = new Set();
118
- /** Highest absolute row index already handed to AG Grid. @internal */
119
- this.deliveredRowCount = 0;
120
118
  /** False until the first message arrives on the subscription. @internal */
121
119
  this.streamStarted = false;
122
120
  /** Whether a MORE_ROWS request is awaiting its batch. @internal */
123
121
  this.moreRowsInFlight = false;
124
122
  /** Whether the current subscription was opened with a CRITERIA_MATCH. @internal */
125
123
  this.hasCriteria = false;
126
- /** So the MAX_VIEW truncation warning is logged once per subscription. @internal */
127
- this.viewTruncationWarned = false;
128
124
  this.createSnapshotFunc = options.createSnapshotFunc;
129
125
  this.createDataserverStreamFunc = options.createDataserverStreamFunc;
130
126
  this.getMoreRowsFunc = options.getMoreRowsFunc;
@@ -371,7 +367,7 @@ export class GenesisInfiniteDatasource {
371
367
  };
372
368
  // The cache may already cover this block - one batch can span several blocks, and blocks are
373
369
  // re-requested after a purge - and once the stream has ended nothing more will arrive.
374
- if (this.streamStarted && (this.rowData.size >= endRow || this.moreRows === false)) {
370
+ if (this.streamStarted && (this.rowData.size >= endRow || this.streamEnded)) {
375
371
  this.settleBlock(block, true);
376
372
  return;
377
373
  }
@@ -395,7 +391,7 @@ export class GenesisInfiniteDatasource {
395
391
  * @internal
396
392
  */
397
393
  pumpMoreRows() {
398
- if (this.moreRowsInFlight || this.moreRows === false || this.pendingBlocks.size === 0) {
394
+ if (this.moreRowsInFlight || this.streamEnded || this.pendingBlocks.size === 0) {
399
395
  return;
400
396
  }
401
397
  // No subscription, or none that has identified itself yet: a MORE_ROWS against a stale
@@ -430,8 +426,6 @@ export class GenesisInfiniteDatasource {
430
426
  if ((error === null || error === void 0 ? void 0 : error.status) === GenesisInfiniteDatasource.HTTP_NOT_FOUND) {
431
427
  logger.warn(`Infinite DATASERVER paging is not available on this connection (MORE_ROWS answered 404, expected over HTTP/feature.forceHttp). ${this.resourceName} is truncated to the ${this.rowData.size} row(s) already delivered; use a websocket connection, or a REQUEST_SERVER resource, to page further.`);
432
428
  this.moreRows = false;
433
- // The MAX_VIEW warning would misdiagnose this truncation as a view cap.
434
- this.viewTruncationWarned = true;
435
429
  this.settleCoveredBlocks();
436
430
  return;
437
431
  }
@@ -445,7 +439,7 @@ export class GenesisInfiniteDatasource {
445
439
  * @internal
446
440
  */
447
441
  handleDataserverMessage(message) {
448
- var _a, _b, _c, _d, _e, _f;
442
+ var _a, _b, _c, _d, _e, _f, _g;
449
443
  const messageType = message === null || message === void 0 ? void 0 : message.MESSAGE_TYPE;
450
444
  if (messageType === MessageType.LOGOFF_ACK || messageType === MessageType.MSG_NACK) {
451
445
  // Report it (same messages as the SSRM dataserver resource) - without this a logged-off
@@ -482,7 +476,8 @@ export class GenesisInfiniteDatasource {
482
476
  this.notifyLiveChange(filtered);
483
477
  return;
484
478
  }
485
- const solicited = this.moreRowsInFlight || this.pendingBlocks.size > 0 || !this.streamStarted;
479
+ const answersMoreRows = this.moreRowsInFlight;
480
+ const solicited = answersMoreRows || this.pendingBlocks.size > 0 || !this.streamStarted;
486
481
  this.moreRowsInFlight = false;
487
482
  if ((message === null || message === void 0 ? void 0 : message.MORE_ROWS) !== undefined) {
488
483
  this.moreRows = message.MORE_ROWS;
@@ -503,10 +498,50 @@ export class GenesisInfiniteDatasource {
503
498
  return;
504
499
  }
505
500
  }
501
+ if (!this.streamEnded) {
502
+ this.notePagingStatus(answersMoreRows && !((_g = filtered === null || filtered === void 0 ? void 0 : filtered.inserts) === null || _g === void 0 ? void 0 : _g.length) && (message === null || message === void 0 ? void 0 : message.MORE_ROWS) === true);
503
+ }
506
504
  this.streamStarted = true;
507
505
  this.settleCoveredBlocks();
508
506
  this.pumpMoreRows();
509
507
  }
508
+ /**
509
+ * Stops the walk when the view is full at MAX_VIEW (checked by row count, which saves asking for
510
+ * the empty page) or a MORE_ROWS came back empty, and says why.
511
+ * @param emptyReply - Whether this message answered a MORE_ROWS with no rows.
512
+ * @internal
513
+ */
514
+ notePagingStatus(emptyReply) {
515
+ var _a;
516
+ const maxView = (_a = this.resourceParams) === null || _a === void 0 ? void 0 : _a.MAX_VIEW;
517
+ const status = dataserverPagingStatus({
518
+ moreRows: this.moreRows,
519
+ emptyReply,
520
+ rowsHeld: this.rowData.size,
521
+ maxView,
522
+ });
523
+ if (status !== 'view-full' && status !== 'stalled') {
524
+ return;
525
+ }
526
+ this.pagingStopped = status;
527
+ const warning = describeDataserverPagingStop({
528
+ status,
529
+ resourceName: this.resourceName,
530
+ rowsHeld: this.rowData.size,
531
+ maxView,
532
+ emptyReply,
533
+ // ROWS_COUNT ignores CRITERIA_MATCH, so it is no total for a filtered view.
534
+ totalRows: this.hasCriteria ? undefined : this.serverRowsCount,
535
+ attribute: 'max-view',
536
+ });
537
+ if (warning) {
538
+ logger.warn(warning);
539
+ }
540
+ }
541
+ /** Whether the stream will deliver no more rows: the data ran out, or paging stopped. @internal */
542
+ get streamEnded() {
543
+ return this.moreRows === false || this.pagingStopped !== undefined;
544
+ }
510
545
  /**
511
546
  * Reports a server-pushed change to the host so the grid can catch up.
512
547
  * @internal
@@ -551,11 +586,13 @@ export class GenesisInfiniteDatasource {
551
586
  }, GenesisInfiniteDatasource.BLOCK_RESOLVE_TIMEOUT_MS);
552
587
  }
553
588
  /**
554
- * Merges a batch into the cumulative cache.
589
+ * Merges a batch's INSERT and MODIFY rows into the cumulative cache.
590
+ * @remarks A DELETE never reaches here: any batch carrying one drops the cache instead (see
591
+ * notifyLiveChange), as removing a delivered row would shift the slice of every later block.
555
592
  * @internal
556
593
  */
557
594
  accumulate(result) {
558
- var _a, _b, _c;
595
+ var _a, _b;
559
596
  const rows = new Map(this.rowData);
560
597
  (_a = result.inserts) === null || _a === void 0 ? void 0 : _a.forEach((row) => {
561
598
  const key = row === null || row === void 0 ? void 0 : row[this.rowId];
@@ -566,41 +603,20 @@ export class GenesisInfiniteDatasource {
566
603
  }
567
604
  rows.set(key, row);
568
605
  });
569
- // Live MODIFY rows may be partial, so merge into the cached row instead of clobbering it.
606
+ // Under a custom row-id a pushed row is identified only by its ROW_REF.
607
+ const resolveKey = cachedRowKeyResolver(rows, this.rowId);
608
+ // Live MODIFY rows may be partial, so merge into the cached row instead of clobbering it. A
609
+ // row resolved through its ROW_REF carries the custom id as undefined; keep the cached one.
570
610
  (_b = result.updates) === null || _b === void 0 ? void 0 : _b.forEach((row) => {
571
- const key = row === null || row === void 0 ? void 0 : row[this.rowId];
611
+ const key = resolveKey(row);
572
612
  if (key === undefined) {
573
613
  return;
574
614
  }
575
615
  const existing = rows.get(key);
576
- rows.set(key, existing ? Object.assign(Object.assign({}, existing), row) : row);
577
- });
578
- (_c = result.deletes) === null || _c === void 0 ? void 0 : _c.forEach((row) => {
579
- const key = row === null || row === void 0 ? void 0 : row[this.rowId];
580
- if (key === undefined) {
581
- return;
582
- }
583
- // Rows already handed to AG Grid sit at fixed absolute indices; removing one from the
584
- // cache would shift the slice offsets of every later block.
585
- const index = this.cacheIndexOf(rows, key);
586
- if (index === -1 || (this.deliveredRowCount > 0 && index < this.deliveredRowCount)) {
587
- return;
588
- }
589
- rows.delete(key);
616
+ rows.set(key, existing ? Object.assign(Object.assign(Object.assign({}, existing), row), { [this.rowId]: key }) : row);
590
617
  });
591
618
  this.rowData = rows;
592
619
  }
593
- /** Position of a key in the cache, or -1. @internal */
594
- cacheIndexOf(rows, key) {
595
- let index = 0;
596
- for (const mapKey of rows.keys()) {
597
- if (mapKey === key) {
598
- return index;
599
- }
600
- index += 1;
601
- }
602
- return -1;
603
- }
604
620
  /**
605
621
  * Settles every pending block the cache now covers.
606
622
  * @internal
@@ -608,7 +624,7 @@ export class GenesisInfiniteDatasource {
608
624
  settleCoveredBlocks() {
609
625
  // Iterate a copy: settling a block removes it from the set.
610
626
  for (const block of [...this.pendingBlocks]) {
611
- if (this.rowData.size >= block.endRow || this.moreRows === false) {
627
+ if (this.rowData.size >= block.endRow || this.streamEnded) {
612
628
  this.settleBlock(block, true);
613
629
  }
614
630
  }
@@ -630,16 +646,15 @@ export class GenesisInfiniteDatasource {
630
646
  return;
631
647
  }
632
648
  const rows = [...this.rowData.values()].slice(block.startRow, block.endRow);
633
- this.deliveredRowCount = Math.max(this.deliveredRowCount, block.startRow + rows.length);
634
- // Once the stream reports no more rows the cache is the whole dataset, filter included.
635
- // Before that, an unfiltered total gives the grid an accurate scrollbar, while a filtered
636
- // view has to stay open-ended - see serverRowsCount.
637
- const lastRow = this.moreRows === false
649
+ // Once the stream has ended the cache is every row the grid can have, filter included.
650
+ // Before that, an unfiltered total gives the grid an accurate scrollbar, capped at MAX_VIEW
651
+ // since the view never holds more, while a filtered view has to stay open-ended - see
652
+ // serverRowsCount.
653
+ const lastRow = this.streamEnded
638
654
  ? this.rowData.size
639
- : this.hasCriteria
655
+ : this.hasCriteria || this.serverRowsCount === undefined
640
656
  ? -1
641
- : ((_a = this.serverRowsCount) !== null && _a !== void 0 ? _a : -1);
642
- this.warnIfViewTruncated();
657
+ : Math.min(this.serverRowsCount, resolveMaxView((_a = this.resourceParams) === null || _a === void 0 ? void 0 : _a.MAX_VIEW));
643
658
  logger.debug('Infinite DATASERVER block settled', {
644
659
  startRow: block.startRow,
645
660
  endRow: block.endRow,
@@ -689,26 +704,6 @@ export class GenesisInfiniteDatasource {
689
704
  (_b = this.onRowsWithdrawnFunc) === null || _b === void 0 ? void 0 : _b.call(this, withdrawnRowIds);
690
705
  }
691
706
  }
692
- /**
693
- * Warns when the view filled up before the data ran out.
694
- * @remarks The server stops sending at MAX_VIEW rows and reports MORE_ROWS false, which is
695
- * indistinguishable from the end of the data - the grid would silently show a truncated view
696
- * and a row count smaller than the resource's. Raising `max-view` past the row count is the
697
- * fix, so say so rather than leaving it to be discovered.
698
- * @internal
699
- */
700
- warnIfViewTruncated() {
701
- var _a, _b;
702
- if (this.viewTruncationWarned ||
703
- this.moreRows !== false ||
704
- this.hasCriteria ||
705
- this.serverRowsCount === undefined ||
706
- this.rowData.size >= this.serverRowsCount) {
707
- return;
708
- }
709
- this.viewTruncationWarned = true;
710
- logger.warn(`${this.resourceName} stopped at ${this.rowData.size} of ${this.serverRowsCount} rows: the DATASERVER view is capped by MAX_VIEW (${(_b = (_a = this.resourceParams) === null || _a === void 0 ? void 0 : _a.MAX_VIEW) !== null && _b !== void 0 ? _b : 'server default'}). Raise max-view past the row count to reach the rest.`);
711
- }
712
707
  /** @internal */
713
708
  failAllPendingBlocks() {
714
709
  // Iterate a copy: settling a block removes it from the set.
@@ -740,12 +735,11 @@ export class GenesisInfiniteDatasource {
740
735
  this.subscriptionKey = undefined;
741
736
  this.sourceRef = undefined;
742
737
  this.rowData = new Map();
743
- this.deliveredRowCount = 0;
744
738
  this.streamStarted = false;
745
739
  this.serverRowsCount = undefined;
746
740
  this.hasCriteria = false;
747
- this.viewTruncationWarned = false;
748
741
  this.moreRows = undefined;
742
+ this.pagingStopped = undefined;
749
743
  this.moreRowsInFlight = false;
750
744
  }
751
745
  /**
@@ -1,5 +1,5 @@
1
1
  import { __awaiter, __decorate } from "tslib";
2
- import { Auth, Connect } from '@genesislcap/foundation-comms';
2
+ import { Auth, Connect, resolveMaxView, } from '@genesislcap/foundation-comms';
3
3
  import { UUID } from '@genesislcap/foundation-utils';
4
4
  import { DeliveredBlockLedger } from './delivered-block.ledger';
5
5
  import { convertDateFilterToCriteria, convertNumberFilterToCriteria, convertSetFilterToCriteria, convertTextFilterToCriteria, } from './filter.utils';
@@ -284,17 +284,20 @@ export class BaseServerSideDatasource {
284
284
  const currentLastRowNumber = params.request.endRow;
285
285
  this.calculatedRowsCount = this.clientRowsCount || currentLastRowNumber;
286
286
  let rowCount;
287
+ // The cap GSF applies: a MAX_VIEW of 0 (what an empty or non-numeric `max-view` arrives as) is
288
+ // an unlimited view, not an empty one, and a missing one is the server default of 1000.
289
+ const viewCap = resolveMaxView(this.maxView);
287
290
  // Infinite scroll: getRows() only resolves a block once the cumulative row cache covers
288
291
  // its full extent or the stream reported no more data, so blocks are either full or final.
289
292
  if (!this.moreRows) {
290
293
  // Dataset ends here: rowData.size is the deduplicated loaded count. Deliberately not
291
294
  // serverRowsCount (GSF's ROWS_COUNT ignores CRITERIA_MATCH) nor clientRowsCount
292
295
  // (non-deduplicated running sum, overcounts on live streams).
293
- rowCount = Math.min(this.rowData.size, this.maxView);
296
+ rowCount = Math.min(this.rowData.size, viewCap);
294
297
  }
295
- else if (this.rowData.size >= this.maxView) {
298
+ else if (this.rowData.size >= viewCap) {
296
299
  // Hard cap - the view can't grow past maxView.
297
- rowCount = this.maxView;
300
+ rowCount = viewCap;
298
301
  }
299
302
  else {
300
303
  // Full block, more may follow: leave undefined so AG Grid keeps requesting blocks
@@ -1,8 +1,8 @@
1
1
  import { __awaiter } from "tslib";
2
- import { MessageType, logger, } from '@genesislcap/foundation-comms';
2
+ import { dataserverPagingStatus, describeDataserverPagingStop, MessageType, logger, } from '@genesislcap/foundation-comms';
3
3
  import { DOM } from '@microsoft/fast-element';
4
4
  import { getAvailableIndexes, getAvailableIndexFields } from '../utils';
5
- import { filterDataserverResult } from './dataserver-result.filter';
5
+ import { cachedRowKeyResolver, filterDataserverResult } from './dataserver-result.filter';
6
6
  import { BaseServerSideDatasource } from './server-side.resource-base';
7
7
  /**
8
8
  * The DATASERVER IServerSideDatasource implementation, used for SSRM (Server-Side Row Model) in the grid.
@@ -69,6 +69,8 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
69
69
  this.structuralPushPending = false;
70
70
  this.streamStarted = false;
71
71
  this.moreRowsInFlight = false;
72
+ this.pagingStopped = undefined;
73
+ this.reportedRowsCount = undefined;
72
74
  yield _super.refreshDatasource.call(this, params);
73
75
  });
74
76
  }
@@ -99,9 +101,10 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
99
101
  ? Number(params.request.endRow)
100
102
  : blockStartRow + Number(this.maxRows);
101
103
  // Infinite scroll keeps a session-cumulative row cache (see handleCurrentStreamLoad);
102
- // when it already covers this block, or the stream ended, no further stream messages
103
- // will arrive for this request - resolve it synchronously from the cache.
104
- const blockAlreadyCovered = this.currentSequenceId >= 1 && (this.rowData.size >= blockEndRow || !this.moreRows);
104
+ // when it already covers this block, or the stream ended or the view filled, no further
105
+ // stream messages will arrive for this request - resolve it synchronously from the cache.
106
+ const blockAlreadyCovered = this.currentSequenceId >= 1 &&
107
+ (this.rowData.size >= blockEndRow || !this.moreRows || this.pagingStopped !== undefined);
105
108
  // A re-read is the moment the grid asks for current rows. When the cache holds a change the
106
109
  // grid never saw (a DELETE it was not told about), serving the slice would hand back the
107
110
  // deleted row, so refresh instead: the grid drops every block and re-reads from the server.
@@ -205,7 +208,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
205
208
  this.completeEmptyResult(params);
206
209
  return;
207
210
  }
208
- if (this.rowData.size >= blockEndRow || !this.moreRows) {
211
+ if (this.rowData.size >= blockEndRow || !this.moreRows || this.pagingStopped !== undefined) {
209
212
  DOM.queueUpdate(() => resolveBlock(safetyTimer));
210
213
  }
211
214
  };
@@ -236,7 +239,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
236
239
  * @internal
237
240
  */
238
241
  dispatchStreamMessage(dataserverResult) {
239
- var _a, _b, _c, _d, _e;
242
+ var _a, _b, _c, _d, _e, _f, _g;
240
243
  const messageType = dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.MESSAGE_TYPE;
241
244
  if (messageType &&
242
245
  (messageType === MessageType.LOGOFF_ACK || messageType === MessageType.MSG_NACK)) {
@@ -245,8 +248,12 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
245
248
  }
246
249
  return;
247
250
  }
251
+ const answersMoreRows = this.moreRowsInFlight;
248
252
  if ((dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.MORE_ROWS) !== undefined) {
249
253
  this.moreRows = dataserverResult.MORE_ROWS;
254
+ // Only replies carry MORE_ROWS, and an empty one carries no ROW either: it still answers
255
+ // the request, so a later push must not be taken for the page.
256
+ this.moreRowsInFlight = false;
250
257
  }
251
258
  if (dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.ROW) {
252
259
  const nextMessage = filterDataserverResult(dataserverResult, this.rowId);
@@ -262,7 +269,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
262
269
  // prepended it to the cache and shifted every later block's slice against the grid.
263
270
  const carriesPushedChanges = ((_b = (_a = nextMessage.updates) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 || ((_d = (_c = nextMessage.deletes) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0;
264
271
  const solicited = !carriesPushedChanges &&
265
- (this.moreRowsInFlight || this.pendingBlocks.size > 0 || !this.streamStarted);
272
+ (answersMoreRows || this.pendingBlocks.size > 0 || !this.streamStarted);
266
273
  this.moreRowsInFlight = false;
267
274
  this.streamStarted = true;
268
275
  this.handleCurrentStreamLoad(nextMessage, solicited);
@@ -272,12 +279,72 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
272
279
  if (this.serverRowsCount === 0) {
273
280
  this.serverRowsCount = (_e = dataserverResult.ROWS_COUNT) !== null && _e !== void 0 ? _e : this.rowData.size;
274
281
  }
282
+ if (typeof dataserverResult.ROWS_COUNT === 'number') {
283
+ this.reportedRowsCount = dataserverResult.ROWS_COUNT;
284
+ }
275
285
  }
276
286
  }
287
+ // Only when paging off the server's cursor: under VIEW_NUMBER pagination GSF sizes the view to
288
+ // a single page, which never fills.
289
+ if (!(Number((_f = this.resourceParams) === null || _f === void 0 ? void 0 : _f.VIEW_NUMBER) > 0)) {
290
+ this.notePagingStatus(answersMoreRows && !((_g = dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.ROW) === null || _g === void 0 ? void 0 : _g.length) && (dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.MORE_ROWS) === true);
291
+ }
277
292
  for (const block of this.pendingBlocks) {
278
293
  block.handle(dataserverResult);
279
294
  }
280
295
  }
296
+ /**
297
+ * Rows the server's view holds, as far as the cache knows: deleted rows kept as tombstones
298
+ * below the delivered watermark have already left it.
299
+ * @internal
300
+ */
301
+ get viewRowsHeld() {
302
+ return this.rowData.size - this.retainedDeletedRowIds.size;
303
+ }
304
+ /**
305
+ * Stops paging when the view is full at MAX_VIEW (checked by row count, which saves asking for
306
+ * the empty page) or a MORE_ROWS came back empty, says why, and pages again once a full view
307
+ * shrinks.
308
+ * @param emptyReply - Whether this message answered a MORE_ROWS with no rows.
309
+ * @internal
310
+ */
311
+ notePagingStatus(emptyReply) {
312
+ const status = dataserverPagingStatus({
313
+ moreRows: this.moreRows,
314
+ emptyReply,
315
+ rowsHeld: this.viewRowsHeld,
316
+ maxView: this.maxView,
317
+ });
318
+ if (status === 'more' && this.pagingStopped === 'view-full') {
319
+ this.pagingStopped = undefined;
320
+ return;
321
+ }
322
+ if ((status !== 'view-full' && status !== 'stalled') || this.pagingStopped) {
323
+ return;
324
+ }
325
+ this.pagingStopped = status;
326
+ const warning = describeDataserverPagingStop({
327
+ status,
328
+ resourceName: this.resourceName,
329
+ rowsHeld: this.viewRowsHeld,
330
+ maxView: this.maxView,
331
+ emptyReply,
332
+ // ROWS_COUNT ignores CRITERIA_MATCH, so it is no total for a filtered view.
333
+ totalRows: this.getResourceParam('CRITERIA_MATCH') ? undefined : this.reportedRowsCount,
334
+ attribute: 'max-view',
335
+ });
336
+ if (warning) {
337
+ logger.warn(warning);
338
+ }
339
+ }
340
+ /**
341
+ * Once paging stops the dataset ends where the cache does, so the grid shows no rows past it.
342
+ * @internal
343
+ */
344
+ getCorrectRowCount(params) {
345
+ const rowCount = super.getCorrectRowCount(params);
346
+ return this.pagingStopped ? this.rowData.size : rowCount;
347
+ }
281
348
  /**
282
349
  * Dataserver-specific sorting setup that validates columns are part of an index.
283
350
  * This is required because dataserver needs indexed columns for efficient sorting.
@@ -354,6 +421,8 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
354
421
  this.structuralPushPending = false;
355
422
  this.streamStarted = false;
356
423
  this.moreRowsInFlight = false;
424
+ this.pagingStopped = undefined;
425
+ this.reportedRowsCount = undefined;
357
426
  yield _super.destroy.call(this);
358
427
  });
359
428
  }
@@ -382,18 +451,21 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
382
451
  this.structuralPushPending = true;
383
452
  }
384
453
  });
454
+ // Under a custom row-id a pushed row is identified only by its ROW_REF.
455
+ const resolveKey = cachedRowKeyResolver(rows, this.rowId);
385
456
  // Live QUERY_UPDATE messages can arrive while a block is pending: MODIFY rows may be
386
- // partial, so merge into the cached row instead of clobbering it.
457
+ // partial, so merge into the cached row instead of clobbering it. A row resolved through its
458
+ // ROW_REF carries the custom id as undefined; keep the cached one.
387
459
  (_b = result.updates) === null || _b === void 0 ? void 0 : _b.forEach((updateData) => {
388
- const key = updateData === null || updateData === void 0 ? void 0 : updateData[this.rowId];
460
+ const key = resolveKey(updateData);
389
461
  if (key === undefined) {
390
462
  return;
391
463
  }
392
464
  const existing = rows.get(key);
393
- rows.set(key, existing ? Object.assign(Object.assign({}, existing), updateData) : updateData);
465
+ rows.set(key, existing ? Object.assign(Object.assign(Object.assign({}, existing), updateData), { [this.rowId]: key }) : updateData);
394
466
  });
395
467
  (_c = result.deletes) === null || _c === void 0 ? void 0 : _c.forEach((deleteData) => {
396
- const key = deleteData === null || deleteData === void 0 ? void 0 : deleteData[this.rowId];
468
+ const key = resolveKey(deleteData);
397
469
  if (key === undefined || !rows.has(key)) {
398
470
  return;
399
471
  }
@@ -1,5 +1,5 @@
1
1
  import { __awaiter, __decorate } from "tslib";
2
- import { MessageType, ResourceType, dataServerResultFilter, normaliseCriteria, } from '@genesislcap/foundation-comms';
2
+ import { MessageType, ResourceType, dataServerResultFilter, dataserverPagingStatus, describeDataserverPagingStop, normaliseCriteria, } from '@genesislcap/foundation-comms';
3
3
  import { LifecycleMixin } from '@genesislcap/foundation-utils';
4
4
  import { DOM, attr, customElement } from '@microsoft/fast-element';
5
5
  import { BehaviorSubject, EMPTY } from 'rxjs';
@@ -60,6 +60,8 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
60
60
  this.requiresFullRowDataAndColDefs = true;
61
61
  this._lastMoreRows = false;
62
62
  this._isMoreRowsResult = false;
63
+ /** How many MORE_ROWS sent by loadMore() are awaiting their reply. @internal */
64
+ this._pendingLoadMore = 0;
63
65
  this._nextOffset = 0;
64
66
  this._loadMoreCount = 0;
65
67
  this.onDatasourceReady = () => {
@@ -195,6 +197,9 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
195
197
  this.clearRowData();
196
198
  this._lastMoreRows = false;
197
199
  this._isMoreRowsResult = false;
200
+ this._pendingLoadMore = 0;
201
+ this._serverMoreRows = undefined;
202
+ this._pagingStopped = undefined;
198
203
  this._nextOffset = 0;
199
204
  this._loadMoreCount = 0;
200
205
  this.$emit(datasourceEventNames.destroy);
@@ -334,6 +339,9 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
334
339
  // Reset load more tracking on reload
335
340
  this._nextOffset = 0;
336
341
  this._loadMoreCount = 0;
342
+ this._pendingLoadMore = 0;
343
+ this._serverMoreRows = undefined;
344
+ this._pagingStopped = undefined;
337
345
  this.datasource.updateLoadMoreCount(0);
338
346
  this.datasource.stream = undefined;
339
347
  this.clearRowData(withoutColumnDefs);
@@ -401,24 +409,22 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
401
409
  return;
402
410
  }
403
411
  this.dataSubWasLoggedOff = false;
404
- if (hasMoreRowsInResult) {
405
- this.updateLoadMoreStatus(result.MORE_ROWS, result.SOURCE_REF);
406
- }
412
+ const answersLoadMore = hasMoreRowsInResult && this.takeLoadMoreReply();
407
413
  if (result.ROW) {
408
- const nextMessage = dataServerResultFilter(result);
409
- this.handleStreamResult(nextMessage);
410
- if (hasMoreRowsInResult && result.MORE_ROWS !== this._lastMoreRows) {
411
- const moreRowsDetail = {
412
- moreRows: result.MORE_ROWS,
413
- sourceRef: result.SOURCE_REF,
414
- };
415
- this.$emit(gridProGenesisDatasourceEventNames.moreRowsChanged, moreRowsDetail);
416
- this._lastMoreRows = result.MORE_ROWS;
417
- }
414
+ this.handleStreamResult(dataServerResultFilter(result));
418
415
  }
419
416
  else {
420
417
  this.handleStreamResult(result);
421
418
  }
419
+ // Read after the rows are applied, so a push that fills or shrinks the view counts.
420
+ const moreRows = this.notePagingStatus(result, hasMoreRowsInResult, answersLoadMore);
421
+ if (moreRows !== undefined && (hasMoreRowsInResult || moreRows !== this._lastMoreRows)) {
422
+ this.updateLoadMoreStatus(moreRows, result.SOURCE_REF);
423
+ // REQUEST_SERVER pages by OFFSET and has never fired more-rows-changed.
424
+ if (!this.isRequestServer) {
425
+ this.emitMoreRowsChanged(moreRows, result.SOURCE_REF);
426
+ }
427
+ }
422
428
  });
423
429
  }
424
430
  catch (error) {
@@ -453,6 +459,75 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
453
459
  this.applyDataserverData(result);
454
460
  }
455
461
  }
462
+ /**
463
+ * Counts off one reply to an outstanding loadMore().
464
+ * @returns Whether a loadMore() was awaiting this reply.
465
+ * @internal
466
+ */
467
+ takeLoadMoreReply() {
468
+ const answersLoadMore = this._pendingLoadMore > 0;
469
+ this._pendingLoadMore = Math.max(0, this._pendingLoadMore - 1);
470
+ return answersLoadMore;
471
+ }
472
+ /**
473
+ * Works out whether more rows can be loaded after a stream message.
474
+ * @remarks On DATASERVER, Load More turns off once the view is full at MAX_VIEW, as every
475
+ * further MORE_ROWS would come back empty (see `dataserverPagingStatus`), or after a MORE_ROWS
476
+ * came back empty while the view had room. It turns back on when a full view shrinks, since GSF
477
+ * fills the freed room on the next MORE_ROWS. A moving view never fills, and under VIEW_NUMBER
478
+ * pagination the view is a single page, so both follow MORE_ROWS alone, as REQUEST_SERVER does.
479
+ * @returns Whether more rows can be loaded, or `undefined` before the server has said.
480
+ * @internal
481
+ */
482
+ notePagingStatus(result, hasMoreRowsInResult, answersLoadMore) {
483
+ var _a;
484
+ if (hasMoreRowsInResult) {
485
+ this._serverMoreRows = result.MORE_ROWS;
486
+ }
487
+ if (this._serverMoreRows === undefined) {
488
+ return undefined;
489
+ }
490
+ if (this.isRequestServer || Number(this.viewNumber) > 0) {
491
+ return hasMoreRowsInResult ? this._serverMoreRows : undefined;
492
+ }
493
+ // The MAX_VIEW the logon actually sent.
494
+ const maxView = +this.maxView;
495
+ const emptyReply = answersLoadMore && result.MORE_ROWS === true && !((_a = result.ROW) === null || _a === void 0 ? void 0 : _a.length);
496
+ const status = dataserverPagingStatus({
497
+ moreRows: this._serverMoreRows,
498
+ emptyReply,
499
+ rowsHeld: this.rowData.size,
500
+ maxView,
501
+ movingView: this.movingView,
502
+ });
503
+ if (status === 'more' && this._pagingStopped === 'view-full') {
504
+ this._pagingStopped = undefined;
505
+ }
506
+ else if ((status === 'view-full' || status === 'stalled') && !this._pagingStopped) {
507
+ this._pagingStopped = status;
508
+ const warning = describeDataserverPagingStop({
509
+ status,
510
+ resourceName: this.resourceName,
511
+ rowsHeld: this.rowData.size,
512
+ maxView,
513
+ emptyReply,
514
+ attribute: 'max-view',
515
+ });
516
+ if (warning) {
517
+ logger.warn(warning);
518
+ }
519
+ }
520
+ return status === 'more' && !this._pagingStopped;
521
+ }
522
+ /** Emits `more-rows-changed` when the availability of more rows changes. @internal */
523
+ emitMoreRowsChanged(moreRows, sourceRef) {
524
+ if (moreRows === this._lastMoreRows) {
525
+ return;
526
+ }
527
+ const moreRowsDetail = { moreRows, sourceRef };
528
+ this.$emit(gridProGenesisDatasourceEventNames.moreRowsChanged, moreRowsDetail);
529
+ this._lastMoreRows = moreRows;
530
+ }
456
531
  /**
457
532
  * Updates the status bar based on more rows status
458
533
  * @param moreRows - Whether more rows are available
@@ -661,6 +736,11 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
661
736
  loadMore() {
662
737
  switch (this.datasource.resourceType) {
663
738
  case ResourceType.DATASERVER:
739
+ // Every further MORE_ROWS comes back empty, and on GSF 8.15 still skips rows.
740
+ if (this._pagingStopped) {
741
+ logger.debug(`loadMore() ignored: paging ${this.resourceName} stopped (${this._pagingStopped})`);
742
+ return;
743
+ }
664
744
  let viewNumber;
665
745
  if (this.viewNumber) {
666
746
  this._loadMoreCount = +this.viewNumber + 1;
@@ -669,7 +749,12 @@ let GridProGenesisDatasource = class GridProGenesisDatasource extends LifecycleM
669
749
  else {
670
750
  this._loadMoreCount += 1;
671
751
  }
672
- this.connect.getMoreRows(this._sourceRef, viewNumber);
752
+ this._pendingLoadMore += 1;
753
+ Promise.resolve(this.connect.getMoreRows(this._sourceRef, viewNumber)).catch((error) => {
754
+ // No reply is coming, so the next message carrying MORE_ROWS does not answer this one.
755
+ this._pendingLoadMore = Math.max(0, this._pendingLoadMore - 1);
756
+ logger.error(`Failed to load more rows for ${this.resourceName}`, error);
757
+ });
673
758
  break;
674
759
  case ResourceType.REQUEST_SERVER:
675
760
  this._loadMoreCount += 1;