@genesislcap/grid-pro 15.24.2 → 15.25.0-FUI-2611.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 (39) hide show
  1. package/dist/custom-elements.json +557 -43
  2. package/dist/dts/datasource/base.datasource.d.ts +70 -7
  3. package/dist/dts/datasource/base.datasource.d.ts.map +1 -1
  4. package/dist/dts/datasource/base.types.d.ts +27 -0
  5. package/dist/dts/datasource/base.types.d.ts.map +1 -1
  6. package/dist/dts/datasource/delivered-block.ledger.d.ts +32 -0
  7. package/dist/dts/datasource/delivered-block.ledger.d.ts.map +1 -0
  8. package/dist/dts/datasource/infinite.datasource.d.ts +33 -1
  9. package/dist/dts/datasource/infinite.datasource.d.ts.map +1 -1
  10. package/dist/dts/datasource/infinite.resource.d.ts +59 -6
  11. package/dist/dts/datasource/infinite.resource.d.ts.map +1 -1
  12. package/dist/dts/datasource/server-side.datasource.d.ts +14 -4
  13. package/dist/dts/datasource/server-side.datasource.d.ts.map +1 -1
  14. package/dist/dts/datasource/server-side.resource-base.d.ts +24 -0
  15. package/dist/dts/datasource/server-side.resource-base.d.ts.map +1 -1
  16. package/dist/dts/datasource/server-side.resource-dataserver.d.ts +26 -0
  17. package/dist/dts/datasource/server-side.resource-dataserver.d.ts.map +1 -1
  18. package/dist/dts/datasource/server-side.resource-reqrep.d.ts.map +1 -1
  19. package/dist/dts/grid-pro-beta.d.ts +6 -0
  20. package/dist/dts/grid-pro-beta.d.ts.map +1 -1
  21. package/dist/dts/grid-pro-genesis-datasource/datasource-events.types.d.ts +4 -0
  22. package/dist/dts/grid-pro-genesis-datasource/datasource-events.types.d.ts.map +1 -1
  23. package/dist/dts/grid-pro.d.ts.map +1 -1
  24. package/dist/dts/react.d.ts +1 -1
  25. package/dist/esm/datasource/base.datasource.js +177 -23
  26. package/dist/esm/datasource/delivered-block.ledger.js +59 -0
  27. package/dist/esm/datasource/infinite.datasource.js +61 -17
  28. package/dist/esm/datasource/infinite.resource.js +59 -10
  29. package/dist/esm/datasource/server-side.datasource.js +73 -25
  30. package/dist/esm/datasource/server-side.resource-base.js +43 -0
  31. package/dist/esm/datasource/server-side.resource-dataserver.js +77 -17
  32. package/dist/esm/datasource/server-side.resource-reqrep.js +12 -0
  33. package/dist/esm/grid-pro-beta.js +20 -2
  34. package/dist/esm/grid-pro.js +4 -1
  35. package/dist/grid-pro.api.json +35 -35
  36. package/dist/grid-pro.d.ts +212 -17
  37. package/dist/react.cjs +6 -6
  38. package/dist/react.mjs +6 -6
  39. package/package.json +13 -13
@@ -1,6 +1,7 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { dataServerResultFilter, MessageType, } from '@genesislcap/foundation-comms';
3
3
  import { logger } from '../utils';
4
+ import { DeliveredBlockLedger } from './delivered-block.ledger';
4
5
  import { convertFilterModelToCriteria } from './filter.utils';
5
6
  /**
6
7
  * Converts an AG Grid sort model to the Genesis ORDER_BY form the resource expects.
@@ -45,6 +46,8 @@ export function convertSortModelToOrderBy(sortModel, resourceIndexes, isRequestS
45
46
  */
46
47
  export class GenesisInfiniteDatasource {
47
48
  constructor(options) {
49
+ /** Set by destroy(); a reply that resolves afterwards belongs to a dropped cache. @internal */
50
+ this.destroyed = false;
48
51
  /**
49
52
  * Session-cumulative row cache, keyed by row id. A Map preserves insertion order, so the
50
53
  * cache can be sliced by block range; Genesis splits batches across messages and later
@@ -69,7 +72,12 @@ export class GenesisInfiniteDatasource {
69
72
  this.getMoreRowsFunc = options.getMoreRowsFunc;
70
73
  this.onRowsInvalidatedFunc = options.onRowsInvalidatedFunc;
71
74
  this.onRowsUpdatedFunc = options.onRowsUpdatedFunc;
75
+ this.onRowsDeliveredFunc = options.onRowsDeliveredFunc;
76
+ this.onRowsWithdrawnFunc = options.onRowsWithdrawnFunc;
77
+ this.onRowsEvictedFunc = options.onRowsEvictedFunc;
78
+ this.onRowCacheResetFunc = options.onRowCacheResetFunc;
72
79
  this.errorHandlerFunc = options.errorHandlerFunc;
80
+ this.ledger = new DeliveredBlockLedger(options.rowId, options.maxBlocksInCache);
73
81
  this.resourceName = options.resourceName;
74
82
  this.resourceParams = Object.assign({}, options.resourceParams);
75
83
  this.resourceIndexes = options.resourceIndexes;
@@ -164,6 +172,7 @@ export class GenesisInfiniteDatasource {
164
172
  const combinedCriteria = [this.baseCriteria, filterCriteria].filter(Boolean).join(' && ');
165
173
  // Build sort config
166
174
  const sortConfig = convertSortModelToOrderBy(params.sortModel, this.resourceIndexes, this.isRequestServer);
175
+ this.noteRequestKey(combinedCriteria, sortConfig);
167
176
  // DATASERVER pages through a persistent subscription; REQUEST_SERVER is stateless and
168
177
  // pages by OFFSET on a fresh request each time.
169
178
  if (!this.isRequestServer) {
@@ -173,6 +182,13 @@ export class GenesisInfiniteDatasource {
173
182
  const requestParams = this.buildRequestServerParams(params.startRow, combinedCriteria, sortConfig);
174
183
  logger.debug('Infinite datasource request params', requestParams);
175
184
  const result = yield this.createSnapshotFunc(requestParams);
185
+ // A reply that lands after destroy() belongs to a cache the grid has already dropped
186
+ // (reloadResourceData purges it); AG Grid ignores the callback, and the host must not
187
+ // learn of rows the grid will never hold.
188
+ if (this.destroyed) {
189
+ logger.debug('Infinite REQUEST_SERVER reply discarded: the datasource was destroyed');
190
+ return;
191
+ }
176
192
  if (!result) {
177
193
  params.failCallback();
178
194
  return;
@@ -181,6 +197,7 @@ export class GenesisInfiniteDatasource {
181
197
  const rowData = result.REPLY;
182
198
  const lastRow = this.getLastRow(result, params.startRow, rowData.length, Boolean(combinedCriteria));
183
199
  params.successCallback(rowData, lastRow);
200
+ this.deliverRows(params.startRow, rowData);
184
201
  return;
185
202
  }
186
203
  // Fallback - empty result
@@ -202,20 +219,31 @@ export class GenesisInfiniteDatasource {
202
219
  * outstanding at once, and each settles as soon as the cache covers its range.
203
220
  * @internal
204
221
  */
222
+ /**
223
+ * Notes the CRITERIA_MATCH/ORDER_BY a block is being read with. A change means AG Grid has
224
+ * purged its cache and restarted from block 0: on DATASERVER the subscription is re-opened
225
+ * (those are DATA_LOGON parameters), and on both transports the host is told the rows it held
226
+ * are gone, so the rows read under the new filter or sort are reported as new.
227
+ * @internal
228
+ */
229
+ noteRequestKey(criteria, sortConfig) {
230
+ var _a;
231
+ const key = JSON.stringify(Object.assign({ criteria }, sortConfig));
232
+ if (this.subscriptionKey !== undefined && this.subscriptionKey !== key) {
233
+ if (!this.isRequestServer) {
234
+ this.teardownDataserverSubscription();
235
+ }
236
+ this.ledger.clear();
237
+ (_a = this.onRowCacheResetFunc) === null || _a === void 0 ? void 0 : _a.call(this);
238
+ }
239
+ this.subscriptionKey = key;
240
+ }
205
241
  getDataserverRows(params, criteria, sortConfig) {
206
242
  if (!this.createDataserverStreamFunc || !this.getMoreRowsFunc) {
207
243
  logger.error('Infinite DATASERVER paging requires a stream and a getMoreRows function');
208
244
  params.failCallback();
209
245
  return;
210
246
  }
211
- // CRITERIA_MATCH and ORDER_BY are DATA_LOGON parameters, so a filter or sort change needs a
212
- // fresh subscription and a fresh cache. AG Grid purges its own cache on those changes and
213
- // restarts from block 0, so nothing is lost.
214
- const key = JSON.stringify(Object.assign({ criteria }, sortConfig));
215
- if (this.subscriptionKey !== undefined && this.subscriptionKey !== key) {
216
- this.teardownDataserverSubscription();
217
- }
218
- this.subscriptionKey = key;
219
247
  const startRow = Number.isFinite(Number(params.startRow)) ? Number(params.startRow) : 0;
220
248
  const endRow = Number.isFinite(Number(params.endRow))
221
249
  ? Number(params.endRow)
@@ -380,7 +408,7 @@ export class GenesisInfiniteDatasource {
380
408
  deletes: (_k = (_j = result.deletes) === null || _j === void 0 ? void 0 : _j.length) !== null && _k !== void 0 ? _k : 0,
381
409
  });
382
410
  this.reset();
383
- (_l = this.onRowsInvalidatedFunc) === null || _l === void 0 ? void 0 : _l.call(this);
411
+ (_l = this.onRowsInvalidatedFunc) === null || _l === void 0 ? void 0 : _l.call(this, result);
384
412
  return;
385
413
  }
386
414
  if (modified) {
@@ -388,7 +416,7 @@ export class GenesisInfiniteDatasource {
388
416
  logger.debug('Infinite DATASERVER live update patched into the cache', {
389
417
  updates: (_o = (_m = result.updates) === null || _m === void 0 ? void 0 : _m.length) !== null && _o !== void 0 ? _o : 0,
390
418
  });
391
- (_p = this.onRowsUpdatedFunc) === null || _p === void 0 ? void 0 : _p.call(this);
419
+ (_p = this.onRowsUpdatedFunc) === null || _p === void 0 ? void 0 : _p.call(this, result);
392
420
  }
393
421
  }
394
422
  /**
@@ -506,6 +534,25 @@ export class GenesisInfiniteDatasource {
506
534
  lastRow,
507
535
  });
508
536
  block.success(rows, lastRow);
537
+ this.deliverRows(block.startRow, rows);
538
+ }
539
+ /**
540
+ * Reports rows handed to the grid for a block, then the rows a re-read of that block no longer
541
+ * carries and the rows whose block the grid evicted.
542
+ * @internal
543
+ */
544
+ deliverRows(startRow, rows) {
545
+ var _a, _b, _c;
546
+ const { withdrawnRowIds, evictedRowIds } = this.ledger.record(startRow, rows);
547
+ if (rows.length > 0) {
548
+ (_a = this.onRowsDeliveredFunc) === null || _a === void 0 ? void 0 : _a.call(this, rows);
549
+ }
550
+ if (withdrawnRowIds.length > 0) {
551
+ (_b = this.onRowsWithdrawnFunc) === null || _b === void 0 ? void 0 : _b.call(this, withdrawnRowIds);
552
+ }
553
+ if (evictedRowIds.length > 0) {
554
+ (_c = this.onRowsEvictedFunc) === null || _c === void 0 ? void 0 : _c.call(this, evictedRowIds);
555
+ }
509
556
  }
510
557
  /**
511
558
  * Warns when the view filled up before the data ran out.
@@ -567,7 +614,9 @@ export class GenesisInfiniteDatasource {
567
614
  this.moreRowsInFlight = false;
568
615
  }
569
616
  destroy() {
617
+ this.destroyed = true;
570
618
  this.teardownDataserverSubscription();
619
+ this.ledger.clear();
571
620
  logger.debug('GenesisInfiniteDatasource destroyed');
572
621
  }
573
622
  }
@@ -1,6 +1,6 @@
1
1
  import { __awaiter, __decorate } from "tslib";
2
2
  import { dataServerResultFilter, Datasource, MessageType, normaliseCriteria, toFieldMetadata, } from '@genesislcap/foundation-comms';
3
- import { JSONSerializer, LifecycleMixin } from '@genesislcap/foundation-utils';
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';
@@ -30,7 +30,7 @@ const withoutColumnDefs = null;
30
30
  * @fires base-datasource-connected - Fired when error state is cleared after connection succeeds
31
31
  * @fires datasource-loading-finished - Fired when pending stream transactions are flushed with no row changes
32
32
  * @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`
33
+ * @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
34
  * @fires cache-filter-config - Fired to persist filter configuration for the grid
35
35
  * @fires refresh-server-side - Fired to request a server-side refresh. detail: `RefreshServerSideEventDetail`
36
36
  * @fires set-server-side-datasource - Fired to attach or clear the server-side row model datasource
@@ -41,7 +41,7 @@ const withoutColumnDefs = null;
41
41
  * @fires datasource-init - Fired when the server-side grid model should initialize data
42
42
  * @fires datasource-schema-updated - Fired when column metadata or defs are updated. detail: `SchemaUpdatedEventDetail`
43
43
  * @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`
44
+ * @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
45
  * @fires apply-server-side-transaction - Fired to apply a server-side row transaction
46
46
  * @fires datasource-ready - Fired when the host grid is ready to start loading data
47
47
  */
@@ -91,6 +91,10 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
91
91
  errorHandlerFunc: this.handleErrors.bind(this),
92
92
  onNoDataAvailableFunc: () => this.$emit(datasourceEventNames.noDataAvailable),
93
93
  onDataAvailableFunc: () => this.$emit(datasourceEventNames.loadingFinished),
94
+ onRowsDeliveredFunc: this.reportDeliveredRows.bind(this),
95
+ onRowsWithdrawnFunc: this.reportWithdrawnRows.bind(this),
96
+ onRowsEvictedFunc: this.forgetRows.bind(this),
97
+ maxBlocksInCache: this.gridMaxBlocksInCache,
94
98
  resourceName: this.resourceName,
95
99
  resourceParams: this.datasource.requestOnlyParams,
96
100
  resourceIndexes: this.indexes,
@@ -107,6 +111,11 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
107
111
  errorHandlerFunc: this.handleErrors.bind(this),
108
112
  onNoDataAvailableFunc: () => this.$emit(datasourceEventNames.noDataAvailable),
109
113
  onDataAvailableFunc: () => this.$emit(datasourceEventNames.loadingFinished),
114
+ onRowsDeliveredFunc: this.reportDeliveredRows.bind(this),
115
+ onRowsWithdrawnFunc: this.reportWithdrawnRows.bind(this),
116
+ onRowsEvictedFunc: this.forgetRows.bind(this),
117
+ maxBlocksInCache: this.gridMaxBlocksInCache,
118
+ gridReceivesLiveUpdates: this.liveUpdates,
110
119
  resourceName: this.resourceName,
111
120
  resourceParams: this.datasource.dataserverOnlyParams,
112
121
  resourceIndexes: this.indexes,
@@ -145,6 +154,16 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
145
154
  */
146
155
  this.hasCapturedFirstPollResult = false;
147
156
  }
157
+ /**
158
+ * The `maxBlocksInCache` the consumer gave the grid, if any, so block eviction can be mirrored
159
+ * in the held-row bookkeeping. The server-side defaults set none, so AG Grid keeps every block.
160
+ * @internal
161
+ */
162
+ get gridMaxBlocksInCache() {
163
+ var _a;
164
+ const value = Number((_a = this.deferredGridOptions) === null || _a === void 0 ? void 0 : _a.maxBlocksInCache);
165
+ return Number.isFinite(value) && value > 0 ? value : undefined;
166
+ }
148
167
  resourceNameChanged(oldValue, newValue) {
149
168
  if (!oldValue || oldValue === newValue)
150
169
  return;
@@ -195,9 +214,12 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
195
214
  */
196
215
  reloadResourceData(params) {
197
216
  return __awaiter(this, void 0, void 0, function* () {
198
- var _a;
217
+ var _a, _b, _c;
199
218
  this.datasource.destroy();
200
- (_a = this.liveUpdatesStream) === null || _a === void 0 ? void 0 : _a.unsubscribe();
219
+ // A block request still in flight was read for the cache being dropped here; its reply must
220
+ // not be reported as rows the grid holds.
221
+ (_b = (_a = this.ssrmDatasource) === null || _a === void 0 ? void 0 : _a.invalidatePendingLoads) === null || _b === void 0 ? void 0 : _b.call(_a);
222
+ (_c = this.liveUpdatesStream) === null || _c === void 0 ? void 0 : _c.unsubscribe();
201
223
  this.liveUpdatesStream = undefined;
202
224
  // Emit event to cache current filter model before clearing data
203
225
  this.$emit(datasourceEventNames.cacheFilterConfig);
@@ -329,20 +351,22 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
329
351
  yield this.init();
330
352
  });
331
353
  }
354
+ /**
355
+ * Forgets every held row and tells consumers, as the client-side datasource does: the grid is
356
+ * dropping its rows (a reload, a filter or sort change, or teardown). `withColumnDefs` also
357
+ * clears the column definitions.
358
+ * @internal
359
+ */
332
360
  clearRowData(withColumnDefs = true) {
333
361
  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: [] };
362
+ this.resetTransaction();
363
+ this.$emit(datasourceEventNames.dataCleared, {
364
+ includeSchema: Boolean(withColumnDefs),
365
+ });
341
366
  // Emit event to apply server side transaction for removing all data
342
367
  this.$emit(datasourceEventNames.applyServerSideTransaction, {
343
- transaction: this.transactionData,
368
+ transaction: { remove: [] },
344
369
  });
345
- this.transactionData = undefined;
346
370
  }
347
371
  /**
348
372
  * Checks whether the resolved resource can be served through the Server-Side Row Model.
@@ -471,7 +495,7 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
471
495
  const rowId = row[this.rowId];
472
496
  if (this.previousPollResult.has(rowId)) {
473
497
  const previousRow = this.previousPollResult.get(rowId);
474
- if (this.serializer.serialize(previousRow) !== this.serializer.serialize(row)) {
498
+ if (this.hasRowContentChanged(previousRow, row)) {
475
499
  rowsToUpdate.push(row);
476
500
  }
477
501
  }
@@ -492,6 +516,17 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
492
516
  removed: rowsToDelete.length,
493
517
  totalInPoll: currentPollMap.size,
494
518
  });
519
+ // The poll has already classified the rows, so report them as they are (the same shape
520
+ // the client-side model emits: a remove carries just the row id) and keep the held rows
521
+ // in step so a later block re-read is classified correctly.
522
+ this.transactionData = {
523
+ add: rowsToInsert,
524
+ update: rowsToUpdate,
525
+ remove: rowsToDelete.map((row) => ({ [this.rowId]: row[this.rowId] })),
526
+ };
527
+ [...rowsToInsert, ...rowsToUpdate].forEach((row) => this.rowData.set(row[this.rowId], row));
528
+ rowsToDelete.forEach((row) => this.rowData.delete(row[this.rowId]));
529
+ this.emitTransaction(this.transactionData);
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
  }
@@ -611,9 +662,6 @@ let GridProServerSideDatasource = class GridProServerSideDatasource extends Life
611
662
  throw new Error('loadMore() method is not supported for server-side datasource');
612
663
  }
613
664
  };
614
- __decorate([
615
- JSONSerializer
616
- ], GridProServerSideDatasource.prototype, "serializer", void 0);
617
665
  __decorate([
618
666
  Datasource
619
667
  ], 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,9 @@ 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;
40
+ this.onRowsEvictedFunc = options.onRowsEvictedFunc;
31
41
  this.resourceName = options.resourceName;
32
42
  this.resourceParams = options.resourceParams;
33
43
  this.originalCriteriaMatch = this.getResourceParam('CRITERIA_MATCH');
@@ -36,6 +46,7 @@ export class BaseServerSideDatasource {
36
46
  this.maxRows = options.maxRows;
37
47
  this.maxView = options.maxView;
38
48
  this.rowId = options.rowId;
49
+ this.ledger = new DeliveredBlockLedger(options.rowId, options.maxBlocksInCache);
39
50
  }
40
51
  /**
41
52
  * Gets a resource parameter, handling both REQUEST_SERVER (DETAILS structure) and DATASERVER (flat structure)
@@ -116,6 +127,7 @@ export class BaseServerSideDatasource {
116
127
  });
117
128
  }
118
129
  destroy() {
130
+ this.invalidatePendingLoads();
119
131
  this.rowData = new Map();
120
132
  this.clientRowsCount = 0;
121
133
  this.calculatedRowsCount = 0;
@@ -125,6 +137,15 @@ export class BaseServerSideDatasource {
125
137
  this.sourceRef = undefined;
126
138
  this.lastSuccessRowData = undefined;
127
139
  }
140
+ /**
141
+ * Marks every block request still in flight as stale: the grid is dropping the cache they were
142
+ * read for, so their replies must not be handed to the host as held rows.
143
+ * @internal
144
+ */
145
+ invalidatePendingLoads() {
146
+ this.loadGeneration += 1;
147
+ this.ledger.clear();
148
+ }
128
149
  refreshDatasource(params) {
129
150
  return __awaiter(this, void 0, void 0, function* () {
130
151
  this.destroy();
@@ -287,6 +308,9 @@ export class BaseServerSideDatasource {
287
308
  completeEmptyResult(params) {
288
309
  var _a, _b;
289
310
  if ((_b = (_a = this.lastSuccessRowData) === null || _a === void 0 ? void 0 : _a.rowData) === null || _b === void 0 ? void 0 : _b.length) {
311
+ // Re-serving the previous block's snapshot keeps the SSRM request from hanging; it is not a
312
+ // delivery of new rows, so it is not reported - the snapshot may predate a poll that has
313
+ // since reported those rows changed or removed.
290
314
  params.success(this.lastSuccessRowData);
291
315
  return;
292
316
  }
@@ -295,6 +319,25 @@ export class BaseServerSideDatasource {
295
319
  params.success(successRowData);
296
320
  this.notifyNoDataAvailableIfEmpty(params, successRowData);
297
321
  }
322
+ /**
323
+ * Reports rows handed to the grid for the block at `startRow`, then the rows a re-read of that
324
+ * block no longer carries and the rows whose block the grid evicted.
325
+ * @internal
326
+ */
327
+ deliverRows(startRow, rows) {
328
+ var _a, _b, _c;
329
+ const delivered = rows !== null && rows !== void 0 ? rows : [];
330
+ const { withdrawnRowIds, evictedRowIds } = this.ledger.record(startRow, delivered);
331
+ if (delivered.length > 0) {
332
+ (_a = this.onRowsDeliveredFunc) === null || _a === void 0 ? void 0 : _a.call(this, delivered);
333
+ }
334
+ if (withdrawnRowIds.length > 0) {
335
+ (_b = this.onRowsWithdrawnFunc) === null || _b === void 0 ? void 0 : _b.call(this, withdrawnRowIds);
336
+ }
337
+ if (evictedRowIds.length > 0) {
338
+ (_c = this.onRowsEvictedFunc) === null || _c === void 0 ? void 0 : _c.call(this, evictedRowIds);
339
+ }
340
+ }
298
341
  /** Invokes `onNoDataAvailableFunc` when the first SSRM block has no rows. @internal */
299
342
  notifyNoDataAvailableIfEmpty(params, successRowData) {
300
343
  var _a;
@@ -9,6 +9,7 @@ import { BaseServerSideDatasource } from './server-side.resource-base';
9
9
  */
10
10
  export class DataserverServerSideDatasource extends BaseServerSideDatasource {
11
11
  constructor(options) {
12
+ var _a;
12
13
  super(options);
13
14
  /**
14
15
  * Blocks awaiting stream messages. Settled blocks remove themselves; destroy() cancels
@@ -23,7 +24,24 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
23
24
  * @internal
24
25
  */
25
26
  this.deliveredRowCount = 0;
27
+ /**
28
+ * Ids of rows the server deleted that stay in the cache because the grid was not told
29
+ * (`live-updates` off) and they sit below the delivered watermark - see
30
+ * handleCurrentStreamLoad. They are not reported to the host as rows the grid holds, and
31
+ * their presence marks the cache out of step, so a re-read purges rather than serves them.
32
+ * @internal
33
+ */
34
+ this.retainedDeletedRowIds = new Set();
35
+ /**
36
+ * Set when the cache holds a change the grid's rows do not reflect (a tombstoned DELETE). The
37
+ * blocks AG Grid holds are still consistent with what it was given, but a re-read of one of
38
+ * them must not be served from the cache: it would hand back stale content, so the datasource
39
+ * refreshes instead and the grid re-reads everything from the server.
40
+ * @internal
41
+ */
42
+ this.cacheOutOfStep = false;
26
43
  this.createDataserverStreamFunc = options.createDataserverStreamFunc;
44
+ this.gridReceivesLiveUpdates = (_a = options.gridReceivesLiveUpdates) !== null && _a !== void 0 ? _a : false;
27
45
  }
28
46
  refreshDatasource(params) {
29
47
  const _super = Object.create(null, {
@@ -36,6 +54,8 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
36
54
  this.dataserverStreamSubscription = undefined;
37
55
  this.dataserverStream = undefined;
38
56
  this.deliveredRowCount = 0;
57
+ this.retainedDeletedRowIds.clear();
58
+ this.cacheOutOfStep = false;
39
59
  yield _super.refreshDatasource.call(this, params);
40
60
  });
41
61
  }
@@ -69,6 +89,16 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
69
89
  // when it already covers this block, or the stream ended, no further stream messages
70
90
  // will arrive for this request - resolve it synchronously from the cache.
71
91
  const blockAlreadyCovered = this.currentSequenceId >= 1 && (this.rowData.size >= blockEndRow || !this.moreRows);
92
+ // A re-read is the moment the grid asks for current rows. When the cache holds a change the
93
+ // grid never saw (a DELETE it was not told about), serving the slice would hand back the
94
+ // deleted row, so refresh instead: the grid drops every block and re-reads from the server.
95
+ // fail() releases the request slot, as for a superseded filter or sort change above.
96
+ if (blockAlreadyCovered && this.cacheOutOfStep) {
97
+ logger.debug(`SSRM block ${blockStartRow}-${blockEndRow} for ${this.resourceName} re-read while the cache is out of step with the grid; refreshing`);
98
+ yield this.refreshDatasource(params);
99
+ params.fail();
100
+ return;
101
+ }
72
102
  if (this.currentSequenceId >= 1 &&
73
103
  !blockAlreadyCovered &&
74
104
  (this.moreRows || params.request.startRow >= Number(this.maxRows))) {
@@ -109,6 +139,7 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
109
139
  successRowData.rowCount = this.getCorrectRowCount(params);
110
140
  this.lastSuccessRowData = successRowData;
111
141
  params.success(successRowData);
142
+ this.deliverRows(blockStartRow, successRowData.rowData.filter((row) => !this.retainedDeletedRowIds.has(row === null || row === void 0 ? void 0 : row[this.rowId])));
112
143
  this.notifyNoDataAvailableIfEmpty(params, successRowData);
113
144
  };
114
145
  const failBlock = (timer) => {
@@ -290,6 +321,8 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
290
321
  (_a = this.dataserverStreamSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe();
291
322
  this.dataserverStreamSubscription = undefined;
292
323
  this.deliveredRowCount = 0;
324
+ this.retainedDeletedRowIds.clear();
325
+ this.cacheOutOfStep = false;
293
326
  yield _super.destroy.call(this);
294
327
  });
295
328
  }
@@ -299,13 +332,31 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
299
332
  return;
300
333
  // Infinite scroll accumulates a session-cumulative cache, as GSF splits batches across
301
334
  // messages and later batches append. Map preserves insertion order, so the cache can be
302
- // sliced by block range.
303
- const rows = new Map(this.rowData);
335
+ // sliced by block range - which only works while the cache and the grid agree on where
336
+ // every delivered row sits, so pushed changes below the delivered watermark are mirrored the
337
+ // way the grid applies them, or deferred to a refresh when the grid is not told.
338
+ let rows = new Map(this.rowData);
339
+ // Rows arriving while a block is pending are the page that block asked for and append in
340
+ // view order. With no block waiting they are a push; the host inserts those at the top of
341
+ // a live grid (its transaction uses addIndex 0), so the cache puts them there too. (A push
342
+ // that lands during a page load is indistinguishable from the page and appends.)
343
+ const pushed = this.pendingBlocks.size === 0 && this.deliveredRowCount > 0;
344
+ const prepended = new Map();
304
345
  (_a = result.inserts) === null || _a === void 0 ? void 0 : _a.forEach((insertData) => {
305
- rows.set(insertData[this.rowId], insertData);
346
+ const key = insertData[this.rowId];
347
+ this.retainedDeletedRowIds.delete(key);
348
+ if (pushed && this.gridReceivesLiveUpdates && !rows.has(key)) {
349
+ prepended.set(key, insertData);
350
+ return;
351
+ }
352
+ rows.set(key, insertData);
306
353
  });
354
+ if (prepended.size > 0) {
355
+ rows = new Map([...prepended, ...rows]);
356
+ this.deliveredRowCount += prepended.size;
357
+ }
307
358
  // 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.
359
+ // partial, so merge into the cached row instead of clobbering it.
309
360
  (_b = result.updates) === null || _b === void 0 ? void 0 : _b.forEach((updateData) => {
310
361
  const key = updateData === null || updateData === void 0 ? void 0 : updateData[this.rowId];
311
362
  if (key === undefined) {
@@ -316,28 +367,37 @@ export class DataserverServerSideDatasource extends BaseServerSideDatasource {
316
367
  });
317
368
  (_c = result.deletes) === null || _c === void 0 ? void 0 : _c.forEach((deleteData) => {
318
369
  const key = deleteData === null || deleteData === void 0 ? void 0 : deleteData[this.rowId];
319
- if (key === undefined) {
370
+ if (key === undefined || !rows.has(key)) {
320
371
  return;
321
372
  }
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;
373
+ const index = this.cacheIndexOf(rows, key);
374
+ if (index < this.deliveredRowCount) {
375
+ if (!this.gridReceivesLiveUpdates) {
376
+ // The grid still shows the row at this index. Dropping it would shift every later
377
+ // block's slice against the grid, so keep its slot and let the next re-read refresh.
378
+ this.retainedDeletedRowIds.add(key);
379
+ this.cacheOutOfStep = true;
380
+ return;
334
381
  }
382
+ // The host's remove transaction re-indexes the grid past this row; follow it.
383
+ this.deliveredRowCount -= 1;
335
384
  }
336
385
  rows.delete(key);
337
386
  });
338
387
  this.clientRowsCount += (_e = (_d = result.inserts) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
339
388
  this.rowData = rows;
340
389
  }
390
+ /** Position of a key in the cache's insertion order. @internal */
391
+ cacheIndexOf(rows, key) {
392
+ let index = 0;
393
+ for (const mapKey of rows.keys()) {
394
+ if (mapKey === key) {
395
+ return index;
396
+ }
397
+ index += 1;
398
+ }
399
+ return -1;
400
+ }
341
401
  }
342
402
  /**
343
403
  * 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,6 +128,7 @@ 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) {