@genesislcap/grid-pro 15.30.0 → 15.30.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.
Files changed (43) hide show
  1. package/dist/custom-elements.json +5837 -5435
  2. package/dist/dts/datasource/base.datasource.d.ts +69 -8
  3. package/dist/dts/datasource/base.datasource.d.ts.map +1 -1
  4. package/dist/dts/datasource/base.types.d.ts +10 -0
  5. package/dist/dts/datasource/base.types.d.ts.map +1 -1
  6. package/dist/dts/datasource/dataserver-result.filter.d.ts +17 -0
  7. package/dist/dts/datasource/dataserver-result.filter.d.ts.map +1 -0
  8. package/dist/dts/datasource/delivered-block.ledger.d.ts +37 -0
  9. package/dist/dts/datasource/delivered-block.ledger.d.ts.map +1 -0
  10. package/dist/dts/datasource/infinite.datasource.d.ts +65 -1
  11. package/dist/dts/datasource/infinite.datasource.d.ts.map +1 -1
  12. package/dist/dts/datasource/infinite.resource.d.ts +136 -6
  13. package/dist/dts/datasource/infinite.resource.d.ts.map +1 -1
  14. package/dist/dts/datasource/server-side.datasource.d.ts +8 -4
  15. package/dist/dts/datasource/server-side.datasource.d.ts.map +1 -1
  16. package/dist/dts/datasource/server-side.resource-base.d.ts +23 -0
  17. package/dist/dts/datasource/server-side.resource-base.d.ts.map +1 -1
  18. package/dist/dts/datasource/server-side.resource-dataserver.d.ts +28 -0
  19. package/dist/dts/datasource/server-side.resource-dataserver.d.ts.map +1 -1
  20. package/dist/dts/datasource/server-side.resource-reqrep.d.ts +1 -1
  21. package/dist/dts/datasource/server-side.resource-reqrep.d.ts.map +1 -1
  22. package/dist/dts/grid-pro-beta.d.ts +6 -0
  23. package/dist/dts/grid-pro-beta.d.ts.map +1 -1
  24. package/dist/dts/grid-pro-genesis-datasource/datasource-events.types.d.ts +4 -0
  25. package/dist/dts/grid-pro-genesis-datasource/datasource-events.types.d.ts.map +1 -1
  26. package/dist/dts/grid-pro.d.ts.map +1 -1
  27. package/dist/dts/react.d.ts +2 -2
  28. package/dist/esm/datasource/base.datasource.js +182 -27
  29. package/dist/esm/datasource/dataserver-result.filter.js +28 -0
  30. package/dist/esm/datasource/delivered-block.ledger.js +52 -0
  31. package/dist/esm/datasource/infinite.datasource.js +131 -20
  32. package/dist/esm/datasource/infinite.resource.js +221 -16
  33. package/dist/esm/datasource/server-side.datasource.js +80 -29
  34. package/dist/esm/datasource/server-side.resource-base.js +39 -0
  35. package/dist/esm/datasource/server-side.resource-dataserver.js +110 -23
  36. package/dist/esm/datasource/server-side.resource-reqrep.js +25 -3
  37. package/dist/esm/grid-pro-beta.js +20 -2
  38. package/dist/esm/grid-pro.js +4 -1
  39. package/dist/grid-pro.api.json +35 -35
  40. package/dist/grid-pro.d.ts +290 -18
  41. package/dist/react.cjs +11 -11
  42. package/dist/react.mjs +11 -11
  43. package/package.json +13 -13
@@ -1,11 +1,12 @@
1
1
  import { __awaiter, __decorate } from "tslib";
2
- import { dataServerResultFilter, Datasource, MessageType, normaliseCriteria, toFieldMetadata, } from '@genesislcap/foundation-comms';
3
- import { JSONSerializer, LifecycleMixin } from '@genesislcap/foundation-utils';
2
+ import { Datasource, MessageType, normaliseCriteria, toFieldMetadata, } from '@genesislcap/foundation-comms';
3
+ import { LifecycleMixin } from '@genesislcap/foundation-utils';
4
4
  import { attr, customElement, DOM, observable } from '@microsoft/fast-element';
5
5
  import { gridProGenesisDatasourceEventNames } from '../grid-pro-genesis-datasource';
6
6
  import { datasourceEventNames, } from '../grid-pro-genesis-datasource/datasource-events.types';
7
7
  import { logger } from '../utils';
8
8
  import { GridProBaseDatasource } from './base.datasource';
9
+ import { filterDataserverResult } from './dataserver-result.filter';
9
10
  import { getFilterByFieldType, getServerSideFilterParamsByFieldType, } from './server-side.grid-definitions';
10
11
  import { DataserverServerSideDatasource } from './server-side.resource-dataserver';
11
12
  import { ReqRepServerSideDatasource } from './server-side.resource-reqrep';
@@ -30,7 +31,7 @@ const withoutColumnDefs = null;
30
31
  * @fires base-datasource-connected - Fired when error state is cleared after connection succeeds
31
32
  * @fires datasource-loading-finished - Fired when pending stream transactions are flushed with no row changes
32
33
  * @fires datasource-no-data-available - Fired when loading finishes with an empty row set
33
- * @fires datasource-data-changed - Fired when row data changes from applied stream transactions. detail: `DataChangedEventDetail`
34
+ * @fires datasource-data-changed - Fired when rows are delivered to the grid, a live stream pushes a change, a poll detects one, or a re-read no longer returns a row; `add` are rows the grid did not hold before, `update` are held rows whose content changed, `remove` carries the row id. detail: `DataChangedEventDetail`
34
35
  * @fires cache-filter-config - Fired to persist filter configuration for the grid
35
36
  * @fires refresh-server-side - Fired to request a server-side refresh. detail: `RefreshServerSideEventDetail`
36
37
  * @fires set-server-side-datasource - Fired to attach or clear the server-side row model datasource
@@ -41,7 +42,7 @@ const withoutColumnDefs = null;
41
42
  * @fires datasource-init - Fired when the server-side grid model should initialize data
42
43
  * @fires datasource-schema-updated - Fired when column metadata or defs are updated. detail: `SchemaUpdatedEventDetail`
43
44
  * @fires datasource-filters-restored - Fired when persisted filters are reapplied
44
- * @fires datasource-data-cleared - Fired when server-side row data is cleared. detail: `DataClearedEventDetail`
45
+ * @fires datasource-data-cleared - Fired when the grid drops every row it held (reload, filter or sort change, destroy); `includeSchema` says whether the columns went too. detail: `DataClearedEventDetail`
45
46
  * @fires apply-server-side-transaction - Fired to apply a server-side row transaction
46
47
  * @fires datasource-ready - Fired when the host grid is ready to start loading data
47
48
  */
@@ -91,6 +92,8 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
91
92
  errorHandlerFunc: this.handleErrors.bind(this),
92
93
  onNoDataAvailableFunc: () => this.$emit(datasourceEventNames.noDataAvailable),
93
94
  onDataAvailableFunc: () => this.$emit(datasourceEventNames.loadingFinished),
95
+ onRowsDeliveredFunc: this.reportDeliveredRows.bind(this),
96
+ onRowsWithdrawnFunc: this.reportWithdrawnRows.bind(this),
94
97
  resourceName: this.resourceName,
95
98
  resourceParams: this.datasource.requestOnlyParams,
96
99
  resourceIndexes: this.indexes,
@@ -107,6 +110,8 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
107
110
  errorHandlerFunc: this.handleErrors.bind(this),
108
111
  onNoDataAvailableFunc: () => this.$emit(datasourceEventNames.noDataAvailable),
109
112
  onDataAvailableFunc: () => this.$emit(datasourceEventNames.loadingFinished),
113
+ onRowsDeliveredFunc: this.reportDeliveredRows.bind(this),
114
+ onRowsWithdrawnFunc: this.reportWithdrawnRows.bind(this),
110
115
  resourceName: this.resourceName,
111
116
  resourceParams: this.datasource.dataserverOnlyParams,
112
117
  resourceIndexes: this.indexes,
@@ -195,9 +200,12 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
195
200
  */
196
201
  reloadResourceData(params) {
197
202
  return __awaiter(this, void 0, void 0, function* () {
198
- var _a;
203
+ var _a, _b, _c;
199
204
  this.datasource.destroy();
200
- (_a = this.liveUpdatesStream) === null || _a === void 0 ? void 0 : _a.unsubscribe();
205
+ // A block request still in flight was read for the cache being dropped here; its reply must
206
+ // not be reported as rows the grid holds.
207
+ (_b = (_a = this.ssrmDatasource) === null || _a === void 0 ? void 0 : _a.invalidatePendingLoads) === null || _b === void 0 ? void 0 : _b.call(_a);
208
+ (_c = this.liveUpdatesStream) === null || _c === void 0 ? void 0 : _c.unsubscribe();
201
209
  this.liveUpdatesStream = undefined;
202
210
  // Emit event to cache current filter model before clearing data
203
211
  this.$emit(datasourceEventNames.cacheFilterConfig);
@@ -329,20 +337,22 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
329
337
  yield this.init();
330
338
  });
331
339
  }
340
+ /**
341
+ * Forgets every held row and tells consumers, as the client-side datasource does: the grid is
342
+ * dropping its rows (a reload, a filter or sort change, or teardown). `withColumnDefs` also
343
+ * clears the column definitions.
344
+ * @internal
345
+ */
332
346
  clearRowData(withColumnDefs = true) {
333
347
  this.rowData = new Map();
334
- if (withColumnDefs) {
335
- // Emit event to clear column definitions
336
- this.$emit(datasourceEventNames.dataCleared, {
337
- includeSchema: true,
338
- });
339
- }
340
- this.transactionData = { remove: [] };
348
+ this.resetTransaction();
349
+ this.$emit(datasourceEventNames.dataCleared, {
350
+ includeSchema: Boolean(withColumnDefs),
351
+ });
341
352
  // Emit event to apply server side transaction for removing all data
342
353
  this.$emit(datasourceEventNames.applyServerSideTransaction, {
343
- transaction: this.transactionData,
354
+ transaction: { remove: [] },
344
355
  });
345
- this.transactionData = undefined;
346
356
  }
347
357
  /**
348
358
  * Checks whether the resolved resource can be served through the Server-Side Row Model.
@@ -471,7 +481,7 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
471
481
  const rowId = row[this.rowId];
472
482
  if (this.previousPollResult.has(rowId)) {
473
483
  const previousRow = this.previousPollResult.get(rowId);
474
- if (this.serializer.serialize(previousRow) !== this.serializer.serialize(row)) {
484
+ if (this.hasRowContentChanged(previousRow, row)) {
475
485
  rowsToUpdate.push(row);
476
486
  }
477
487
  }
@@ -492,6 +502,31 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
492
502
  removed: rowsToDelete.length,
493
503
  totalInPoll: currentPollMap.size,
494
504
  });
505
+ // The poll has already classified the rows against the previous poll, so re-classify the
506
+ // inserts against the rows actually held: a row shifted into the polled page from a block
507
+ // above is new to the poll but not to the grid, and reporting it as `add` would duplicate
508
+ // it for consumers.
509
+ const transaction = this.resetTransaction();
510
+ rowsToInsert.forEach((row) => {
511
+ const rowId = row[this.rowId];
512
+ const known = this.rowData.get(rowId);
513
+ if (!known) {
514
+ transaction.add.push(row);
515
+ }
516
+ else if (this.hasRowContentChanged(known, row)) {
517
+ transaction.update.push(row);
518
+ }
519
+ });
520
+ transaction.update.push(...rowsToUpdate);
521
+ transaction.remove.push(...rowsToDelete.map((row) => ({ [this.rowId]: row[this.rowId] })));
522
+ [...rowsToInsert, ...rowsToUpdate].forEach((row) => this.rowData.set(row[this.rowId], row));
523
+ rowsToDelete.forEach((row) => this.rowData.delete(row[this.rowId]));
524
+ this.emitTransaction(transaction);
525
+ // A consumer's handler may have reloaded or destroyed this datasource synchronously; the
526
+ // grid is then being purged and the transaction belongs to rows it no longer holds.
527
+ if (this.transactionData !== transaction) {
528
+ return;
529
+ }
495
530
  this.$emit(datasourceEventNames.applyServerSideTransaction, {
496
531
  transaction: {
497
532
  add: rowsToInsert,
@@ -514,25 +549,41 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
514
549
  this.ssrmDatasource.serverRowsCount += netRowChange;
515
550
  this.ssrmDatasource.clientRowsCount += netRowChange;
516
551
  }
517
- this.transactionData = { add: [], remove: [], update: [] };
552
+ const transaction = this.resetTransaction();
518
553
  this.handleStreamInserts(inserts);
519
554
  this.handleStreamUpdates(updates);
520
555
  this.handleStreamDeletes(deletes);
521
- if (this.transactionData.add.length > 0) {
556
+ // A MODIFY for a row this element has not seen delivered is not reported to consumers (they
557
+ // do not hold it), but it still goes to the grid, which ignores rows outside its store: the
558
+ // stream pre-merge above made it a full row, and the grid may hold it from a block loaded
559
+ // before this element started mirroring deliveries.
560
+ const gridOnlyUpdates = updates.filter((row) => {
561
+ const rowId = row === null || row === void 0 ? void 0 : row[this.rowId];
562
+ return rowId !== undefined && rowId !== null && !this.rowData.has(rowId);
563
+ });
564
+ // Consumers hear the change first; the grid transactions below then catch the grid up.
565
+ this.emitTransaction(transaction);
566
+ // A consumer's handler may have reloaded or destroyed this datasource synchronously; the
567
+ // grid is then being purged and the transaction belongs to rows it no longer holds.
568
+ if (this.transactionData !== transaction) {
569
+ return;
570
+ }
571
+ if (transaction.add.length > 0) {
522
572
  this.$emit(datasourceEventNames.applyServerSideTransaction, {
523
573
  transaction: {
524
574
  route: [],
525
- add: this.transactionData.add,
526
- addIndex: this.reverse ? 0 : this.transactionData.addIndex,
575
+ add: transaction.add,
576
+ addIndex: this.reverse ? 0 : transaction.addIndex,
527
577
  },
528
578
  });
529
579
  }
530
- if (this.transactionData.update.length > 0 || this.transactionData.remove.length > 0) {
580
+ const gridUpdates = [...transaction.update, ...gridOnlyUpdates];
581
+ if (gridUpdates.length > 0 || transaction.remove.length > 0) {
531
582
  this.$emit(datasourceEventNames.applyServerSideTransaction, {
532
583
  transaction: {
533
584
  route: [],
534
- update: this.transactionData.update,
535
- remove: this.transactionData.remove,
585
+ update: gridUpdates,
586
+ remove: transaction.remove,
536
587
  },
537
588
  });
538
589
  }
@@ -563,9 +614,12 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
563
614
  // a complete row — without this, unchanged columns are cleared on every update.
564
615
  const rowData = this.ssrmDatasource.rowData;
565
616
  const mergedRows = normalizedRows.map((row) => {
566
- var _a;
617
+ var _a, _b;
567
618
  const details = (_a = row.DETAILS) !== null && _a !== void 0 ? _a : {};
568
- const rowRef = details[this.rowId];
619
+ // Genesis keys pushes by the default id. Under a custom `row-id` DETAILS carries no
620
+ // custom id at all, so fall back to the held row that carries this ROW_REF.
621
+ const defaultRowId = this.defaultRowIdByResourceType;
622
+ const rowRef = ((_b = details[this.rowId]) !== null && _b !== void 0 ? _b : this.resolveRowId(Object.assign(Object.assign({}, row), { [defaultRowId]: details[defaultRowId] })));
569
623
  const existingRow = rowRef != null ? rowData === null || rowData === void 0 ? void 0 : rowData.get(rowRef) : undefined;
570
624
  if (!existingRow)
571
625
  return row;
@@ -577,7 +631,7 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
577
631
  return Object.assign(Object.assign({}, merged), { DETAILS: details });
578
632
  });
579
633
  const normalizedMessage = Object.assign(Object.assign({}, rawMessage), { ROW: mergedRows });
580
- const nextMessage = dataServerResultFilter(normalizedMessage, this.rowId);
634
+ const nextMessage = filterDataserverResult(normalizedMessage, this.rowId);
581
635
  this.applyLiveServerSideUpdate(nextMessage);
582
636
  });
583
637
  const onError = (error) => {
@@ -611,9 +665,6 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
611
665
  throw new Error('loadMore() method is not supported for server-side datasource');
612
666
  }
613
667
  };
614
- __decorate([
615
- JSONSerializer
616
- ], GridProServerSideDatasource.prototype, "serializer", void 0);
617
668
  __decorate([
618
669
  Datasource
619
670
  ], GridProServerSideDatasource.prototype, "pollingDatasource", void 0);
@@ -1,6 +1,7 @@
1
1
  import { __awaiter, __decorate } from "tslib";
2
2
  import { Auth, Connect } from '@genesislcap/foundation-comms';
3
3
  import { UUID } from '@genesislcap/foundation-utils';
4
+ import { DeliveredBlockLedger } from './delivered-block.ledger';
4
5
  import { convertDateFilterToCriteria, convertNumberFilterToCriteria, convertSetFilterToCriteria, convertTextFilterToCriteria, } from './filter.utils';
5
6
  /**
6
7
  * Base class for server-side resource datasources used in Grid Pro SSRM implementations.
@@ -11,6 +12,12 @@ import { convertDateFilterToCriteria, convertNumberFilterToCriteria, convertSetF
11
12
  export class BaseServerSideDatasource {
12
13
  constructor(options) {
13
14
  this.rowData = new Map();
15
+ /**
16
+ * Bumped whenever the rows the grid holds are dropped (destroy, refresh, reload). A req/rep
17
+ * reply that resolves for an earlier generation belongs to a cache the grid no longer has.
18
+ * @internal
19
+ */
20
+ this.loadGeneration = 0;
14
21
  /**
15
22
  * The number of rows in the grid. THIS DO NOT CONSIDER CRITERIA_MATCH scenarios.
16
23
  * @internal
@@ -28,6 +35,8 @@ export class BaseServerSideDatasource {
28
35
  this.errorHandlerFunc = options.errorHandlerFunc;
29
36
  this.onNoDataAvailableFunc = options.onNoDataAvailableFunc;
30
37
  this.onDataAvailableFunc = options.onDataAvailableFunc;
38
+ this.onRowsDeliveredFunc = options.onRowsDeliveredFunc;
39
+ this.onRowsWithdrawnFunc = options.onRowsWithdrawnFunc;
31
40
  this.resourceName = options.resourceName;
32
41
  this.resourceParams = options.resourceParams;
33
42
  this.originalCriteriaMatch = this.getResourceParam('CRITERIA_MATCH');
@@ -36,6 +45,7 @@ export class BaseServerSideDatasource {
36
45
  this.maxRows = options.maxRows;
37
46
  this.maxView = options.maxView;
38
47
  this.rowId = options.rowId;
48
+ this.ledger = new DeliveredBlockLedger(options.rowId);
39
49
  }
40
50
  /**
41
51
  * Gets a resource parameter, handling both REQUEST_SERVER (DETAILS structure) and DATASERVER (flat structure)
@@ -116,6 +126,7 @@ export class BaseServerSideDatasource {
116
126
  });
117
127
  }
118
128
  destroy() {
129
+ this.invalidatePendingLoads();
119
130
  this.rowData = new Map();
120
131
  this.clientRowsCount = 0;
121
132
  this.calculatedRowsCount = 0;
@@ -125,6 +136,15 @@ export class BaseServerSideDatasource {
125
136
  this.sourceRef = undefined;
126
137
  this.lastSuccessRowData = undefined;
127
138
  }
139
+ /**
140
+ * Marks every block request still in flight as stale: the grid is dropping the cache they were
141
+ * read for, so their replies must not be handed to the host as held rows.
142
+ * @internal
143
+ */
144
+ invalidatePendingLoads() {
145
+ this.loadGeneration += 1;
146
+ this.ledger.clear();
147
+ }
128
148
  refreshDatasource(params) {
129
149
  return __awaiter(this, void 0, void 0, function* () {
130
150
  this.destroy();
@@ -287,6 +307,9 @@ export class BaseServerSideDatasource {
287
307
  completeEmptyResult(params) {
288
308
  var _a, _b;
289
309
  if ((_b = (_a = this.lastSuccessRowData) === null || _a === void 0 ? void 0 : _a.rowData) === null || _b === void 0 ? void 0 : _b.length) {
310
+ // Re-serving the previous block's snapshot keeps the SSRM request from hanging; it is not a
311
+ // delivery of new rows, so it is not reported - the snapshot may predate a poll that has
312
+ // since reported those rows changed or removed.
290
313
  params.success(this.lastSuccessRowData);
291
314
  return;
292
315
  }
@@ -295,6 +318,22 @@ export class BaseServerSideDatasource {
295
318
  params.success(successRowData);
296
319
  this.notifyNoDataAvailableIfEmpty(params, successRowData);
297
320
  }
321
+ /**
322
+ * Reports rows handed to the grid for the block at `startRow`, then the rows a re-read of that
323
+ * block no longer carries and the rows whose block the grid evicted.
324
+ * @internal
325
+ */
326
+ deliverRows(startRow, rows) {
327
+ var _a, _b;
328
+ const delivered = rows !== null && rows !== void 0 ? rows : [];
329
+ const { withdrawnRowIds } = this.ledger.record(startRow, delivered);
330
+ if (delivered.length > 0) {
331
+ (_a = this.onRowsDeliveredFunc) === null || _a === void 0 ? void 0 : _a.call(this, delivered);
332
+ }
333
+ if (withdrawnRowIds.length > 0) {
334
+ (_b = this.onRowsWithdrawnFunc) === null || _b === void 0 ? void 0 : _b.call(this, withdrawnRowIds);
335
+ }
336
+ }
298
337
  /** Invokes `onNoDataAvailableFunc` when the first SSRM block has no rows. @internal */
299
338
  notifyNoDataAvailableIfEmpty(params, successRowData) {
300
339
  var _a;
@@ -1,13 +1,24 @@
1
1
  import { __awaiter } from "tslib";
2
- import { MessageType, dataServerResultFilter, logger, } from '@genesislcap/foundation-comms';
2
+ import { 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
6
  import { BaseServerSideDatasource } from './server-side.resource-base';
6
7
  /**
7
8
  * The DATASERVER IServerSideDatasource implementation, used for SSRM (Server-Side Row Model) in the grid.
8
9
  * @beta
9
10
  */
10
11
  export class DataserverServerSideDatasource extends BaseServerSideDatasource {
12
+ /**
13
+ * Whether the cache holds a change the grid's rows do not reflect (a tombstoned DELETE), in
14
+ * which case a block must not be served from it: the slice would hand back a deleted row.
15
+ * @remarks Derived rather than a flag of its own, so an INSERT that brings a tombstoned id
16
+ * back clears it along with the tombstone instead of leaving the cache marked stale forever.
17
+ * @internal
18
+ */
19
+ get cacheOutOfStep() {
20
+ return this.retainedDeletedRowIds.size > 0 || this.structuralPushPending;
21
+ }
11
22
  constructor(options) {
12
23
  super(options);
13
24
  /**
@@ -23,6 +34,24 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
23
34
  * @internal
24
35
  */
25
36
  this.deliveredRowCount = 0;
37
+ /**
38
+ * Ids of rows the server deleted that stay in the cache because the grid was not told
39
+ * (`live-updates` off) and they sit below the delivered watermark - see
40
+ * handleCurrentStreamLoad. They are not reported to the host as rows the grid holds, and
41
+ * their presence marks the cache out of step, so a re-read purges rather than serves them.
42
+ * @internal
43
+ */
44
+ this.retainedDeletedRowIds = new Set();
45
+ /**
46
+ * Set when a pushed INSERT landed after rows were handed to the grid, so the cache's insertion
47
+ * order no longer matches the grid's row indices. Cleared when the cache is rebuilt.
48
+ * @internal
49
+ */
50
+ this.structuralPushPending = false;
51
+ /** Whether the first batch of the current subscription has arrived. @internal */
52
+ this.streamStarted = false;
53
+ /** Whether a MORE_ROWS request is awaiting its batch. @internal */
54
+ this.moreRowsInFlight = false;
26
55
  this.createDataserverStreamFunc = options.createDataserverStreamFunc;
27
56
  }
28
57
  refreshDatasource(params) {
@@ -36,6 +65,10 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
36
65
  this.dataserverStreamSubscription = undefined;
37
66
  this.dataserverStream = undefined;
38
67
  this.deliveredRowCount = 0;
68
+ this.retainedDeletedRowIds.clear();
69
+ this.structuralPushPending = false;
70
+ this.streamStarted = false;
71
+ this.moreRowsInFlight = false;
39
72
  yield _super.refreshDatasource.call(this, params);
40
73
  });
41
74
  }
@@ -69,6 +102,16 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
69
102
  // when it already covers this block, or the stream ended, no further stream messages
70
103
  // will arrive for this request - resolve it synchronously from the cache.
71
104
  const blockAlreadyCovered = this.currentSequenceId >= 1 && (this.rowData.size >= blockEndRow || !this.moreRows);
105
+ // A re-read is the moment the grid asks for current rows. When the cache holds a change the
106
+ // grid never saw (a DELETE it was not told about), serving the slice would hand back the
107
+ // deleted row, so refresh instead: the grid drops every block and re-reads from the server.
108
+ // fail() releases the request slot, as for a superseded filter or sort change above.
109
+ if (blockAlreadyCovered && this.cacheOutOfStep) {
110
+ logger.debug(`SSRM block ${blockStartRow}-${blockEndRow} for ${this.resourceName} re-read while the cache is out of step with the grid; refreshing`);
111
+ yield this.refreshDatasource(params);
112
+ params.fail();
113
+ return;
114
+ }
72
115
  if (this.currentSequenceId >= 1 &&
73
116
  !blockAlreadyCovered &&
74
117
  (this.moreRows || params.request.startRow >= Number(this.maxRows))) {
@@ -76,6 +119,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
76
119
  ? Number(this.resourceParams.VIEW_NUMBER)
77
120
  : 0;
78
121
  if (Number.isFinite(viewNumber)) {
122
+ this.moreRowsInFlight = true;
79
123
  this.connect.getMoreRows(this.sourceRef, viewNumber);
80
124
  }
81
125
  }
@@ -109,6 +153,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
109
153
  successRowData.rowCount = this.getCorrectRowCount(params);
110
154
  this.lastSuccessRowData = successRowData;
111
155
  params.success(successRowData);
156
+ this.deliverRows(blockStartRow, successRowData.rowData.filter((row) => !this.retainedDeletedRowIds.has(row === null || row === void 0 ? void 0 : row[this.rowId])));
112
157
  this.notifyNoDataAvailableIfEmpty(params, successRowData);
113
158
  };
114
159
  const failBlock = (timer) => {
@@ -191,7 +236,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
191
236
  * @internal
192
237
  */
193
238
  dispatchStreamMessage(dataserverResult) {
194
- var _a;
239
+ var _a, _b, _c, _d, _e;
195
240
  const messageType = dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.MESSAGE_TYPE;
196
241
  if (messageType &&
197
242
  (messageType === MessageType.LOGOFF_ACK || messageType === MessageType.MSG_NACK)) {
@@ -204,13 +249,28 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
204
249
  this.moreRows = dataserverResult.MORE_ROWS;
205
250
  }
206
251
  if (dataserverResult === null || dataserverResult === void 0 ? void 0 : dataserverResult.ROW) {
207
- const nextMessage = dataServerResultFilter(dataserverResult, this.rowId);
208
- this.handleCurrentStreamLoad(nextMessage);
252
+ const nextMessage = filterDataserverResult(dataserverResult, this.rowId);
253
+ // A DATASERVER subscription is live by definition, so every batch is either the page a
254
+ // block asked for or a change the server pushed. Replies only ever carry view rows
255
+ // (INSERTs), so a batch holding a MODIFY or a DELETE is a push whenever it arrives.
256
+ // Otherwise the batch is the reply we are waiting on: a request is outstanding
257
+ // (`moreRowsInFlight`), a block is pending, or this is the logon batch.
258
+ //
259
+ // `pendingBlocks.size === 0` alone is not enough: the 10s safety valve settles a block
260
+ // short while its MORE_ROWS is still outstanding, and GSF splits one reply across
261
+ // messages, so the real page can arrive with no block waiting. Treating that as a push
262
+ // prepended it to the cache and shifted every later block's slice against the grid.
263
+ 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
+ const solicited = !carriesPushedChanges &&
265
+ (this.moreRowsInFlight || this.pendingBlocks.size > 0 || !this.streamStarted);
266
+ this.moreRowsInFlight = false;
267
+ this.streamStarted = true;
268
+ this.handleCurrentStreamLoad(nextMessage, solicited);
209
269
  this.currentSequenceId = dataserverResult.SEQUENCE_ID;
210
270
  if (this.currentSequenceId === 1) {
211
271
  this.sourceRef = dataserverResult.SOURCE_REF;
212
272
  if (this.serverRowsCount === 0) {
213
- this.serverRowsCount = (_a = dataserverResult.ROWS_COUNT) !== null && _a !== void 0 ? _a : this.rowData.size;
273
+ this.serverRowsCount = (_e = dataserverResult.ROWS_COUNT) !== null && _e !== void 0 ? _e : this.rowData.size;
214
274
  }
215
275
  }
216
276
  }
@@ -290,22 +350,40 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
290
350
  (_a = this.dataserverStreamSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe();
291
351
  this.dataserverStreamSubscription = undefined;
292
352
  this.deliveredRowCount = 0;
353
+ this.retainedDeletedRowIds.clear();
354
+ this.structuralPushPending = false;
355
+ this.streamStarted = false;
356
+ this.moreRowsInFlight = false;
293
357
  yield _super.destroy.call(this);
294
358
  });
295
359
  }
296
- handleCurrentStreamLoad(result) {
360
+ handleCurrentStreamLoad(result, solicited) {
297
361
  var _a, _b, _c, _d, _e;
298
362
  if (!result)
299
363
  return;
300
364
  // Infinite scroll accumulates a session-cumulative cache, as GSF splits batches across
301
365
  // messages and later batches append. Map preserves insertion order, so the cache can be
302
- // sliced by block range.
366
+ // sliced by block range - which only works while the cache and the grid agree on where
367
+ // every delivered row sits, so a pushed change below the delivered watermark is kept out of
368
+ // the delivered region and deferred to a refresh.
303
369
  const rows = new Map(this.rowData);
304
370
  (_a = result.inserts) === null || _a === void 0 ? void 0 : _a.forEach((insertData) => {
305
- rows.set(insertData[this.rowId], insertData);
371
+ const key = insertData[this.rowId];
372
+ this.retainedDeletedRowIds.delete(key);
373
+ // A key the cache already holds keeps its slot, so nothing shifts - that is how a DELETE
374
+ // that was tombstoned and then re-inserted puts the cache back in step with the grid.
375
+ const isNewToCache = !rows.has(key);
376
+ rows.set(key, insertData);
377
+ if (!solicited && isNewToCache && this.deliveredRowCount > 0) {
378
+ // A page appends in view order, so it keeps the cache and the grid aligned. A push does
379
+ // not: nothing in the batch says where the server sorts the new row, and the host puts
380
+ // it at the top of a live grid, so from here on the cache cannot be sliced against the
381
+ // grid's indices. Mark it, and let the next read of a covered block refresh instead.
382
+ this.structuralPushPending = true;
383
+ }
306
384
  });
307
385
  // Live QUERY_UPDATE messages can arrive while a block is pending: MODIFY rows may be
308
- // partial, so merge into the cached row instead of clobbering it; DELETE rows drop out.
386
+ // partial, so merge into the cached row instead of clobbering it.
309
387
  (_b = result.updates) === null || _b === void 0 ? void 0 : _b.forEach((updateData) => {
310
388
  const key = updateData === null || updateData === void 0 ? void 0 : updateData[this.rowId];
311
389
  if (key === undefined) {
@@ -316,28 +394,37 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
316
394
  });
317
395
  (_c = result.deletes) === null || _c === void 0 ? void 0 : _c.forEach((deleteData) => {
318
396
  const key = deleteData === null || deleteData === void 0 ? void 0 : deleteData[this.rowId];
319
- if (key === undefined) {
397
+ if (key === undefined || !rows.has(key)) {
320
398
  return;
321
399
  }
322
- // Rows already handed to AG Grid sit at fixed absolute indices; removing them from
323
- // the cumulative cache would shift slice() offsets for later blocks.
324
- if (this.deliveredRowCount > 0) {
325
- let index = 0;
326
- for (const mapKey of rows.keys()) {
327
- if (mapKey === key) {
328
- if (index < this.deliveredRowCount) {
329
- return;
330
- }
331
- break;
332
- }
333
- index += 1;
334
- }
400
+ const index = this.cacheIndexOf(rows, key);
401
+ if (index < this.deliveredRowCount) {
402
+ // The grid still shows the row at this index. Dropping it would shift every later
403
+ // block's slice against the grid, so keep its slot and let the next re-read refresh.
404
+ //
405
+ // Whether the host also removed it from the grid is not knowable here: the host applies
406
+ // live updates from a second, independent DATA_LOGON whose view is not paged with this
407
+ // one, so a delete this stream sees need not have reached the grid at all. Keeping the
408
+ // slot is correct either way - the refresh reconciles both.
409
+ this.retainedDeletedRowIds.add(key);
410
+ return;
335
411
  }
336
412
  rows.delete(key);
337
413
  });
338
414
  this.clientRowsCount += (_e = (_d = result.inserts) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
339
415
  this.rowData = rows;
340
416
  }
417
+ /** Position of a key in the cache's insertion order. @internal */
418
+ cacheIndexOf(rows, key) {
419
+ let index = 0;
420
+ for (const mapKey of rows.keys()) {
421
+ if (mapKey === key) {
422
+ return index;
423
+ }
424
+ index += 1;
425
+ }
426
+ return -1;
427
+ }
341
428
  }
342
429
  /**
343
430
  * Max time a non-paginated SSRM block may stay pending before the safety valve in
@@ -39,6 +39,9 @@ export class ReqRepServerSideDatasource extends BaseServerSideDatasource {
39
39
  }
40
40
  getRows(params) {
41
41
  return __awaiter(this, void 0, void 0, function* () {
42
+ // Taken before the first await: a reload that lands anywhere between here and the reply
43
+ // means the block cache this request was read for is gone.
44
+ const generation = this.loadGeneration;
42
45
  if (yield this.setupFiltering(params)) {
43
46
  params.fail();
44
47
  return;
@@ -54,6 +57,14 @@ export class ReqRepServerSideDatasource extends BaseServerSideDatasource {
54
57
  DETAILS: this.buildRequestDetails(),
55
58
  };
56
59
  const requestResult = yield this.createReqRepRequestFunc(requestParams);
60
+ // The cache this block was read for was dropped while the request was in flight (a filter,
61
+ // sort or criteria change): the reply must not be handed to the host as rows the grid holds.
62
+ // fail() still frees AG Grid's concurrent-request slot; the purge already dropped the block.
63
+ if (generation !== this.loadGeneration) {
64
+ logger.debug('SSRM req/rep reply discarded: the block cache was reset while it was in flight');
65
+ params.fail();
66
+ return;
67
+ }
57
68
  // Apply the result to the grid
58
69
  this.applyServerSideData(params, requestResult);
59
70
  // If polling enabled, schedule stream creation after user stops scrolling
@@ -117,13 +128,18 @@ export class ReqRepServerSideDatasource extends BaseServerSideDatasource {
117
128
  successRowData.rowCount = this.getCorrectRowCount(params);
118
129
  this.lastSuccessRowData = successRowData;
119
130
  params.success(successRowData);
131
+ this.deliverRows(params.request.startRow, successRowData.rowData);
120
132
  this.notifyNoDataAvailableIfEmpty(params, successRowData);
121
133
  }
122
134
  getCorrectRowCount(params) {
123
135
  let rowCount;
124
136
  if (this.moreRows) {
125
- // More data available, return -1 to indicate unknown total
126
- rowCount = -1;
137
+ // More data may follow, so the total is not known yet. It has to be left undefined:
138
+ // `rowCount` is a row count, not the infinite row model's `lastRow`, so -1 is taken
139
+ // literally. A store told it holds -1 rows never reaches the block it just received,
140
+ // so AG Grid asks for the same blocks again as fast as it can - a request loop that
141
+ // pins the main thread and floods the server with REQUEST_SERVER snapshots.
142
+ rowCount = undefined;
127
143
  }
128
144
  else {
129
145
  // Use discovered total if available, otherwise calculate from current position
@@ -281,7 +297,13 @@ export class ReqRepServerSideDatasource extends BaseServerSideDatasource {
281
297
  return {
282
298
  CRITERIA_MATCH: this.resourceParams.DETAILS.CRITERIA_MATCH,
283
299
  MAX_ROWS: this.resourceParams.DETAILS.MAX_ROWS,
284
- ORDER_BY: this.resourceParams.DETAILS.ORDER_BY,
300
+ // Fall back to the row id when the user has not sorted, as the infinite datasource does.
301
+ // OFFSET paging is only correct over a stable order: an unsorted req/rep returns rows in
302
+ // an unspecified order that differs between requests, so consecutive pages overlap and
303
+ // skip. AG Grid then receives the same row id in two blocks, refuses the store it just
304
+ // built, and re-requests both blocks - a loop that runs as fast as the server can answer.
305
+ // The row id is unique and always sortable, giving every page a deterministic window.
306
+ ORDER_BY: this.resourceParams.DETAILS.ORDER_BY || `${this.rowId} ASC`,
285
307
  OFFSET: this.resourceParams.DETAILS.OFFSET,
286
308
  };
287
309
  }
@@ -484,11 +484,25 @@ export class GridProBeta extends LifecycleMixin(FoundationElement) {
484
484
  */
485
485
  handleDataChanged(event) {
486
486
  const { changes } = event.detail;
487
- if (!this.gridApi)
487
+ // Every datasource emits this for consumers, but only the client-side row model takes row
488
+ // transactions: the infinite and server-side datasources drive the grid through
489
+ // refresh-infinite-cache / apply-server-side-transaction, and applyTransaction on those
490
+ // models is a no-op that logs an AG Grid warning per event.
491
+ if (!this.gridApi || !this.isClientSideRowModel)
488
492
  return;
489
493
  const result = this.gridApi.applyTransaction(changes);
490
494
  this.flashAddedCells(result === null || result === void 0 ? void 0 : result.add);
491
495
  }
496
+ /**
497
+ * Whether the live grid runs AG Grid's client-side row model (the default when the datasource
498
+ * set no `rowModelType`).
499
+ * @internal
500
+ */
501
+ get isClientSideRowModel() {
502
+ var _a, _b, _c;
503
+ const rowModelType = (_c = (_b = (_a = this.gridApi) === null || _a === void 0 ? void 0 : _a.getGridOption) === null || _b === void 0 ? void 0 : _b.call(_a, 'rowModelType')) !== null && _c !== void 0 ? _c : 'clientSide';
504
+ return rowModelType === 'clientSide';
505
+ }
492
506
  flashAddedCells(rowNodes) {
493
507
  if (this.enableRowFlashing && (rowNodes === null || rowNodes === void 0 ? void 0 : rowNodes.length)) {
494
508
  this.gridApi.flashCells({ rowNodes });
@@ -601,7 +615,11 @@ export class GridProBeta extends LifecycleMixin(FoundationElement) {
601
615
  if (includeSchema) {
602
616
  this.gridApi.setGridOption('columnDefs', []);
603
617
  }
604
- this.gridApi.setGridOption('rowData', []);
618
+ // rowData belongs to the client-side row model; the infinite and server-side caches are
619
+ // cleared by their own refresh / transaction events.
620
+ if (this.isClientSideRowModel) {
621
+ this.gridApi.setGridOption('rowData', []);
622
+ }
605
623
  this.gridApi.refreshCells({ force: true });
606
624
  }
607
625
  /**