@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
@@ -2,6 +2,7 @@ import { __awaiter, __decorate } from "tslib";
2
2
  import { Connect, ConnectEvents, Datasource, normaliseCriteria, toFieldMetadata, } from '@genesislcap/foundation-comms';
3
3
  import { LifecycleMixin } from '@genesislcap/foundation-utils';
4
4
  import { customElement, DOM, observable } from '@microsoft/fast-element';
5
+ import { debounceTime, skip } from 'rxjs/operators';
5
6
  import { gridProGenesisDatasourceEventNames } from '../grid-pro-genesis-datasource';
6
7
  import { datasourceEventNames, } from '../grid-pro-genesis-datasource/datasource-events.types';
7
8
  import { logger } from '../utils';
@@ -9,6 +10,8 @@ import { GridProBaseDatasource } from './base.datasource';
9
10
  import { GenesisInfiniteDatasource } from './infinite.resource';
10
11
  import { getFilterByFieldType, getServerSideFilterParamsByFieldType, } from './server-side.grid-definitions';
11
12
  const criteriaDelimiter = ';';
13
+ /** How long a burst of `setFilter()` calls is collapsed for, matching the client-side model. */
14
+ const FILTER_DEBOUNCE_MS = 600;
12
15
  /**
13
16
  * A Genesis Datasource element for AG Grid's Infinite Row Model.
14
17
  * @remarks
@@ -40,12 +43,15 @@ const criteriaDelimiter = ';';
40
43
  * @fires datasource-initialize - Fired to hand off infinite row model grid options. detail: `InitializeEventDetail`
41
44
  * @fires datasource-init - Fired when the infinite grid model should initialize data
42
45
  * @fires datasource-schema-updated - Fired when column metadata or defs are updated. detail: `SchemaUpdatedEventDetail`
46
+ * @fires datasource-data-changed - Fired when rows are delivered to the grid, a DATASERVER push changes them, or a re-read no longer returns them; `add` are rows the grid did not hold before, `update` are held rows whose content changed, `remove` carries the row id. detail: `DataChangedEventDetail`
43
47
  * @fires set-infinite-datasource - Fired to attach or clear the infinite row model datasource. detail: `SetInfiniteDatasourceEventDetail`
44
48
  * @fires refresh-infinite-cache - Fired to request an infinite row model refresh; `purge` drops the block cache and re-reads. detail: `RefreshInfiniteCacheEventDetail`
45
49
  * @fires add-grid-css-class - Fired to add a CSS class on the grid host (hover sort indicators). detail: `GridCssClassEventDetail`
46
50
  * @fires remove-grid-css-class - Fired to remove that CSS class from the grid host. detail: `GridCssClassEventDetail`
47
51
  * @fires cache-filter-config - Fired to persist filter configuration for the grid
48
- * @fires datasource-data-cleared - Fired when infinite row data is cleared. detail: `DataClearedEventDetail`
52
+ * @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`
53
+ * @fires datasource-loading-finished - Fired when the first block comes back with rows
54
+ * @fires datasource-no-data-available - Fired when the first block comes back empty
49
55
  * @fires datasource-filters-restored - Fired when persisted filters are reapplied
50
56
  */
51
57
  let GridProInfiniteDatasource = class GridProInfiniteDatasource extends LifecycleMixin(GridProBaseDatasource) {
@@ -61,6 +67,10 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
61
67
  this.setDisconnected(true);
62
68
  return;
63
69
  }
70
+ if (!this.isInfiniteSupportedForResource()) {
71
+ this.clearRowData();
72
+ return;
73
+ }
64
74
  this.$emit(gridProGenesisDatasourceEventNames.dataInit);
65
75
  this.setDisconnected(false);
66
76
  this.indexes = this.getResourceIndexes(this.datasource.availableIndexes, this.datasource.availableSortableFields);
@@ -78,15 +88,14 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
78
88
  // VIEW_NUMBER 0 means "pagination disabled": the server continues from the cursor it
79
89
  // holds on the last row it delivered, rather than re-seeking by offset.
80
90
  getMoreRowsFunc: (sourceRef) => this.connect.getMoreRows(sourceRef, 0),
81
- // A pushed INSERT/DELETE changes which rows exist or their order, so the rows are
82
- // re-read (purge); a pushed MODIFY is already patched into the cache, so the grid only
83
- // needs to re-render its blocks.
84
- onRowsInvalidatedFunc: () => this.$emit(datasourceEventNames.refreshInfiniteCache, {
85
- purge: true,
86
- }),
87
- onRowsUpdatedFunc: () => this.$emit(datasourceEventNames.refreshInfiniteCache, {
88
- purge: false,
89
- }),
91
+ onRowsDeliveredFunc: this.reportDeliveredRows.bind(this),
92
+ onRowsWithdrawnFunc: this.reportWithdrawnRows.bind(this),
93
+ // A filter or sort change drops every block: forget the rows and tell consumers.
94
+ onRowCacheResetFunc: () => this.clearRowData(false),
95
+ onRowsInvalidatedFunc: this.handleLiveRowsInvalidated.bind(this),
96
+ onRowsUpdatedFunc: this.handleLiveRowsUpdated.bind(this),
97
+ onNoDataAvailableFunc: () => this.$emit(datasourceEventNames.noDataAvailable),
98
+ onDataAvailableFunc: () => this.$emit(datasourceEventNames.loadingFinished),
90
99
  errorHandlerFunc: this.handleErrors.bind(this),
91
100
  resourceName: this.resourceName,
92
101
  resourceParams: this.isRequestServer
@@ -96,8 +105,12 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
96
105
  resourceColDefs: this.datasource.originalFieldDef,
97
106
  maxRows: +this.maxRows,
98
107
  rowId: this.rowId,
99
- baseCriteria: this.criteria,
108
+ // The merged criteria, so a criteria added through setFilter() is read with the rows
109
+ // rather than dropped: the logon builder replaces CRITERIA_MATCH with what it is given.
110
+ baseCriteria: this.buildCriteria(),
100
111
  isRequestServer: this.isRequestServer,
112
+ defaultOrderBy: this.orderBy,
113
+ defaultReverse: this.reverse,
101
114
  });
102
115
  // Emit event to set infinite datasource
103
116
  this.$emit(datasourceEventNames.setInfiniteDatasource, {
@@ -140,6 +153,7 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
140
153
  return;
141
154
  this.init();
142
155
  this.subscribeToConnection();
156
+ this.subscribeToFilterChanges();
143
157
  });
144
158
  }
145
159
  disconnectedCallback() {
@@ -150,8 +164,34 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
150
164
  return;
151
165
  yield this.destroy();
152
166
  this.unsubscribeFromConnection();
167
+ this.unsubscribeFromFilterChanges();
153
168
  }));
154
169
  }
170
+ /**
171
+ * Reloads when `setFilter()` or `removeFilter()` changes the criteria.
172
+ * @remarks The criteria a block is read with is captured when the resource is built, so a
173
+ * filter added later reaches nothing until the rows are re-read. The `criteria` attribute
174
+ * already reloads through `criteriaChanged`; this gives the programmatic API the same
175
+ * behaviour instead of silently doing nothing.
176
+ * @internal
177
+ */
178
+ subscribeToFilterChanges() {
179
+ this.unsubscribeFromFilterChanges();
180
+ // Same shape as the client-side datasource: `update` is a BehaviorSubject, so skip(1) drops
181
+ // the value it replays on subscribe (the state init() is already reading), and the debounce
182
+ // collapses a burst of setFilter calls into one reload.
183
+ this.filterSub = this.update.pipe(skip(1), debounceTime(FILTER_DEBOUNCE_MS)).subscribe(() => {
184
+ if (!this.infiniteDatasource)
185
+ return;
186
+ this.reloadResourceData();
187
+ });
188
+ }
189
+ /** @internal */
190
+ unsubscribeFromFilterChanges() {
191
+ var _a;
192
+ (_a = this.filterSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
193
+ this.filterSub = undefined;
194
+ }
155
195
  deepClone() {
156
196
  const copy = super.deepClone();
157
197
  copy.deferredGridOptions = structuredClone(this.deferredGridOptions);
@@ -169,7 +209,10 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
169
209
  // The paging DATA_LOGON is opened through connect.stream, not this.datasource, so destroying
170
210
  // the comms datasource below does not close it. Dispose the IDatasource explicitly or every
171
211
  // criteria change (e.g. a bound search box) leaks a live subscription and its block timers.
172
- (_b = (_a = this.infiniteDatasource) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
212
+ // detach(), not destroy(): AG Grid keeps calling this bean until the replacement is attached
213
+ // a round trip later, and the purge below re-requests blocks immediately. destroy() alone
214
+ // would leave it reading with the old criteria and reporting those rows as delivered.
215
+ (_b = (_a = this.infiniteDatasource) === null || _a === void 0 ? void 0 : _a.detach) === null || _b === void 0 ? void 0 : _b.call(_a);
173
216
  this.infiniteDatasource = undefined;
174
217
  this.datasource.destroy();
175
218
  // Emit event to cache current filter model before clearing data
@@ -225,6 +268,69 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
225
268
  clearReadyListener() {
226
269
  this.removeEventListener(datasourceEventNames.ready, this.onDatasourceReady);
227
270
  }
271
+ /**
272
+ * Whether the infinite row model can drive this resource.
273
+ * @remarks It pages a req/rep by sending CRITERIA_MATCH, ORDER_BY and OFFSET in DETAILS, which
274
+ * a request reply only accepts when it is declared `criteriaOnlyRequest`
275
+ * (`CRITERIA_ONLY_REQUEST` in its metadata). Without it the server expects its inputs in
276
+ * `REQUEST` instead and rejects every block, which on this row model surfaces as a grid that
277
+ * simply never fills. Report it once, up front, as the server-side model does.
278
+ * @returns `true` when the infinite model can be used, `false` after reporting a
279
+ * `resource-type` error.
280
+ * @internal
281
+ */
282
+ isInfiniteSupportedForResource() {
283
+ if (!this.isRequestServer || this.datasource.criteriaOnlyRequest) {
284
+ return true;
285
+ }
286
+ this.handleErrors(`Infinite row model is not supported for the REQUEST_SERVER resource '${this.resourceName}' because it does not support criteria-only requests. Set 'criteriaOnlyRequest = true' on the request reply definition, or use a client-side datasource for this resource.`, 'resource-type');
287
+ return false;
288
+ }
289
+ /**
290
+ * A pushed INSERT/DELETE changes which rows exist or their order, so the change is reported
291
+ * and then the rows are re-read (purge).
292
+ * @internal
293
+ */
294
+ handleLiveRowsInvalidated(change) {
295
+ this.reportLiveChange(change);
296
+ this.$emit(datasourceEventNames.refreshInfiniteCache, {
297
+ purge: true,
298
+ });
299
+ }
300
+ /**
301
+ * A pushed MODIFY is already patched into the paging cache, so the change is reported and the
302
+ * grid only needs to re-render its blocks.
303
+ * @internal
304
+ */
305
+ handleLiveRowsUpdated(change) {
306
+ this.reportLiveChange(change);
307
+ this.$emit(datasourceEventNames.refreshInfiniteCache, {
308
+ purge: false,
309
+ });
310
+ }
311
+ /**
312
+ * Turns a DATASERVER push into the same transaction the client-side datasource reports:
313
+ * partial MODIFY rows are merged into the held row, and a remove carries just the row id.
314
+ * @remarks Unlike the client-side model, a pushed row need not be held here - it may sit
315
+ * beyond the loaded blocks, in which case the grid will never show it.
316
+ *
317
+ * So a pushed INSERT is deliberately not reported: nothing here says where the new row sorts,
318
+ * and reporting an `add` for a row that lands at position 40,000 of an unscrolled grid would
319
+ * contradict the event's contract and leave the row held forever, since no block re-read ever
320
+ * delivers (or withdraws) it. Every INSERT is followed by a purge and re-read, which reports
321
+ * the row as `add` if and when it actually reaches the grid.
322
+ *
323
+ * A MODIFY for a row that is not held is skipped for the same reason; a DELETE is reported by
324
+ * the id it carries, and one that cannot be resolved to an id is skipped with a warning rather
325
+ * than matched against the wrong row.
326
+ * @internal
327
+ */
328
+ reportLiveChange(change) {
329
+ const transaction = this.resetTransaction();
330
+ this.handleStreamUpdates(change.updates);
331
+ this.handleStreamDeletes(change.deletes);
332
+ this.emitTransaction(transaction);
333
+ }
228
334
  destroy() {
229
335
  return __awaiter(this, void 0, void 0, function* () {
230
336
  var _a, _b;
@@ -237,7 +343,9 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
237
343
  this.datasource.destroy();
238
344
  this.clearRowData();
239
345
  if (this.infiniteDatasource) {
240
- (_b = (_a = this.infiniteDatasource).destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
346
+ // The host is done with this bean for good, so detach rather than destroy - see
347
+ // reloadResourceData.
348
+ (_b = (_a = this.infiniteDatasource).detach) === null || _b === void 0 ? void 0 : _b.call(_a);
241
349
  this.infiniteDatasource = undefined;
242
350
  }
243
351
  // Emit events instead of direct grid access
@@ -252,15 +360,18 @@ let GridProInfiniteDatasource = class GridProInfiniteDatasource extends Lifecycl
252
360
  yield this.init();
253
361
  });
254
362
  }
363
+ /**
364
+ * Forgets every held row and tells consumers, as the client-side datasource does: the grid is
365
+ * dropping its rows (a reload, a filter or sort change, or teardown). `withColumnDefs` also
366
+ * clears the column definitions.
367
+ * @internal
368
+ */
255
369
  clearRowData(withColumnDefs = true) {
256
370
  this.rowData = new Map();
257
- if (withColumnDefs) {
258
- // Emit event to clear column definitions
259
- this.$emit(datasourceEventNames.dataCleared, {
260
- includeSchema: true,
261
- });
262
- }
263
- this.transactionData = { remove: [] };
371
+ this.resetTransaction();
372
+ this.$emit(datasourceEventNames.dataCleared, {
373
+ includeSchema: withColumnDefs,
374
+ });
264
375
  }
265
376
  /**
266
377
  * Honours `pollTriggerEvents` without polling: a commit ACK for one of the listed events
@@ -1,6 +1,8 @@
1
1
  import { __awaiter } from "tslib";
2
- import { dataServerResultFilter, MessageType, } from '@genesislcap/foundation-comms';
2
+ import { MessageType, } from '@genesislcap/foundation-comms';
3
3
  import { logger } from '../utils';
4
+ import { filterDataserverResult } from './dataserver-result.filter';
5
+ import { DeliveredBlockLedger } from './delivered-block.ledger';
4
6
  import { convertFilterModelToCriteria } from './filter.utils';
5
7
  /**
6
8
  * Converts an AG Grid sort model to the Genesis ORDER_BY form the resource expects.
@@ -39,12 +41,71 @@ export function convertSortModelToOrderBy(sortModel, resourceIndexes, isRequestS
39
41
  logger.warn(`Column '${colId}' is not part of any named INDEX. Server-side sorting is not available for this column.`);
40
42
  return { orderBy: null, reverse: false };
41
43
  }
44
+ /**
45
+ * Turns whatever a rejected resource read produced into a message worth showing.
46
+ * @remarks Genesis rejects with a message object (`{ ERROR: [{ TEXT }], MESSAGE_TYPE }`), not an
47
+ * `Error`, so stringifying it naively yields "[object Object]" in the grid's error dialog.
48
+ * @internal
49
+ */
50
+ export function describeResourceError(error) {
51
+ var _a, _b, _c;
52
+ if (error instanceof Error) {
53
+ return error.message;
54
+ }
55
+ if (typeof error === 'string') {
56
+ return error;
57
+ }
58
+ const details = error;
59
+ for (const candidate of [details, details === null || details === void 0 ? void 0 : details.receivedMessage]) {
60
+ const texts = (_a = candidate === null || candidate === void 0 ? void 0 : candidate.ERROR) === null || _a === void 0 ? void 0 : _a.map((entry) => entry === null || entry === void 0 ? void 0 : entry.TEXT).filter(Boolean);
61
+ if (texts === null || texts === void 0 ? void 0 : texts.length) {
62
+ return texts.join('; ');
63
+ }
64
+ }
65
+ if (typeof (details === null || details === void 0 ? void 0 : details.message) === 'string' && details.message) {
66
+ return details.message;
67
+ }
68
+ const messageType = (_b = details === null || details === void 0 ? void 0 : details.MESSAGE_TYPE) !== null && _b !== void 0 ? _b : (_c = details === null || details === void 0 ? void 0 : details.receivedMessage) === null || _c === void 0 ? void 0 : _c.MESSAGE_TYPE;
69
+ if (messageType) {
70
+ return messageType;
71
+ }
72
+ try {
73
+ return JSON.stringify(error);
74
+ }
75
+ catch (_d) {
76
+ return String(error);
77
+ }
78
+ }
42
79
  /**
43
80
  * AG Grid Infinite Row Model datasource implementation.
44
81
  * This class implements the IDatasource interface required by AG Grid's Infinite Row Model.
45
82
  */
46
83
  export class GenesisInfiniteDatasource {
47
84
  constructor(options) {
85
+ var _a;
86
+ /** The `reverse` attribute, applied alongside `defaultOrderBy` on DATASERVER. @internal */
87
+ this.defaultReverse = false;
88
+ /**
89
+ * Bumped whenever the rows this datasource was reading for are dropped (a teardown, or a
90
+ * filter/sort change). A REQUEST_SERVER reply that resolves for an earlier generation was read
91
+ * for a cache the grid no longer has.
92
+ * @remarks Deliberately a generation rather than a "destroyed" flag: AG Grid calls `destroy()`
93
+ * on the datasource bean it then keeps using (`InfiniteRowModel.start()` re-applies the
94
+ * `datasource` grid option, and `setDatasource` destroys the previous one first), so a sticky
95
+ * flag would disable the instance the grid is still driving.
96
+ * @internal
97
+ */
98
+ this.loadGeneration = 0;
99
+ /**
100
+ * Set once the host has dropped this instance for a replacement (a criteria change, a resource
101
+ * change, teardown). AG Grid keeps calling the bean it was given until the new one is attached,
102
+ * and its purge re-requests blocks immediately, so without this the replaced instance keeps
103
+ * reading with its captured criteria and reporting those rows to the host as delivered.
104
+ * @remarks Distinct from `destroy()`, which AG Grid itself calls on a bean it then keeps using;
105
+ * only the host calls `detach()`, and only when it will never use this instance again.
106
+ * @internal
107
+ */
108
+ this.detached = false;
48
109
  /**
49
110
  * Session-cumulative row cache, keyed by row id. A Map preserves insertion order, so the
50
111
  * cache can be sliced by block range; Genesis splits batches across messages and later
@@ -69,7 +130,11 @@ export class GenesisInfiniteDatasource {
69
130
  this.getMoreRowsFunc = options.getMoreRowsFunc;
70
131
  this.onRowsInvalidatedFunc = options.onRowsInvalidatedFunc;
71
132
  this.onRowsUpdatedFunc = options.onRowsUpdatedFunc;
133
+ this.onRowsDeliveredFunc = options.onRowsDeliveredFunc;
134
+ this.onRowsWithdrawnFunc = options.onRowsWithdrawnFunc;
135
+ this.onRowCacheResetFunc = options.onRowCacheResetFunc;
72
136
  this.errorHandlerFunc = options.errorHandlerFunc;
137
+ this.ledger = new DeliveredBlockLedger(options.rowId);
73
138
  this.resourceName = options.resourceName;
74
139
  this.resourceParams = Object.assign({}, options.resourceParams);
75
140
  this.resourceIndexes = options.resourceIndexes;
@@ -78,6 +143,10 @@ export class GenesisInfiniteDatasource {
78
143
  this.rowId = options.rowId;
79
144
  this.baseCriteria = options.baseCriteria || '';
80
145
  this.isRequestServer = options.isRequestServer;
146
+ this.defaultOrderBy = options.defaultOrderBy || undefined;
147
+ this.defaultReverse = (_a = options.defaultReverse) !== null && _a !== void 0 ? _a : false;
148
+ this.onNoDataAvailableFunc = options.onNoDataAvailableFunc;
149
+ this.onDataAvailableFunc = options.onDataAvailableFunc;
81
150
  }
82
151
  /**
83
152
  * Builds request parameters for REQUEST_SERVER resources.
@@ -153,6 +222,15 @@ export class GenesisInfiniteDatasource {
153
222
  */
154
223
  getRows(params) {
155
224
  return __awaiter(this, void 0, void 0, function* () {
225
+ var _a, _b;
226
+ // The host has replaced this instance; the grid is only still asking because the replacement
227
+ // has not been attached yet. Reading would use this instance's captured criteria and report
228
+ // rows the grid will drop. fail() releases AG Grid's request slot for the new datasource.
229
+ if (this.detached) {
230
+ logger.debug('Infinite getRows ignored: the datasource was detached by its host');
231
+ params.failCallback();
232
+ return;
233
+ }
156
234
  logger.debug('Infinite getRows called', {
157
235
  startRow: params.startRow,
158
236
  endRow: params.endRow,
@@ -163,7 +241,8 @@ export class GenesisInfiniteDatasource {
163
241
  const filterCriteria = convertFilterModelToCriteria(params.filterModel, this.resourceColDefs);
164
242
  const combinedCriteria = [this.baseCriteria, filterCriteria].filter(Boolean).join(' && ');
165
243
  // Build sort config
166
- const sortConfig = convertSortModelToOrderBy(params.sortModel, this.resourceIndexes, this.isRequestServer);
244
+ const sortConfig = this.resolveSortConfig(params.sortModel);
245
+ this.noteRequestKey(combinedCriteria, sortConfig);
167
246
  // DATASERVER pages through a persistent subscription; REQUEST_SERVER is stateless and
168
247
  // pages by OFFSET on a fresh request each time.
169
248
  if (!this.isRequestServer) {
@@ -172,25 +251,51 @@ export class GenesisInfiniteDatasource {
172
251
  }
173
252
  const requestParams = this.buildRequestServerParams(params.startRow, combinedCriteria, sortConfig);
174
253
  logger.debug('Infinite datasource request params', requestParams);
254
+ // Taken after noteRequestKey, so this is the generation the request belongs to: only a
255
+ // reset that lands while it is in flight invalidates the reply.
256
+ const generation = this.loadGeneration;
175
257
  const result = yield this.createSnapshotFunc(requestParams);
258
+ // The rows this block was read for have since been dropped (a filter or sort change): AG
259
+ // Grid has purged the block, and the host must not learn of rows the grid will never hold.
260
+ // The block is still failed rather than left hanging - AG Grid caps concurrent requests
261
+ // and frees a slot only on success or fail, so a silent return stops it ever asking again.
262
+ if (generation !== this.loadGeneration) {
263
+ logger.debug('Infinite REQUEST_SERVER reply discarded: the block cache was reset');
264
+ params.failCallback();
265
+ return;
266
+ }
176
267
  if (!result) {
177
268
  params.failCallback();
178
269
  return;
179
270
  }
271
+ // A rejected request answers with a message, not a REPLY. Settling it as an empty block
272
+ // would tell the grid the dataset simply ends here, so the user sees a short grid with no
273
+ // indication that anything failed.
274
+ const messageType = result.MESSAGE_TYPE;
275
+ if (messageType === MessageType.LOGOFF_ACK || messageType === MessageType.MSG_NACK) {
276
+ const message = messageType === MessageType.LOGOFF_ACK
277
+ ? `Connection lost to ${this.resourceName}`
278
+ : `Request rejected by ${this.resourceName}`;
279
+ logger.error(`Infinite REQUEST_SERVER block failed: ${message}`, result);
280
+ (_a = this.errorHandlerFunc) === null || _a === void 0 ? void 0 : _a.call(this, message, 'connection');
281
+ params.failCallback();
282
+ return;
283
+ }
180
284
  if ('REPLY' in result && Array.isArray(result.REPLY)) {
181
285
  const rowData = result.REPLY;
182
286
  const lastRow = this.getLastRow(result, params.startRow, rowData.length, Boolean(combinedCriteria));
183
287
  params.successCallback(rowData, lastRow);
288
+ this.deliverRows(params.startRow, rowData);
289
+ this.notifyDataAvailability(params.startRow, rowData.length);
184
290
  return;
185
291
  }
186
292
  // Fallback - empty result
187
293
  params.successCallback([], params.startRow);
294
+ this.notifyDataAvailability(params.startRow, 0);
188
295
  }
189
296
  catch (error) {
190
297
  logger.error('Error in infinite getRows:', error);
191
- if (this.errorHandlerFunc) {
192
- this.errorHandlerFunc(error instanceof Error ? error.message : String(error), 'unknown');
193
- }
298
+ (_b = this.errorHandlerFunc) === null || _b === void 0 ? void 0 : _b.call(this, describeResourceError(error), 'unknown');
194
299
  params.failCallback();
195
300
  }
196
301
  });
@@ -202,20 +307,57 @@ export class GenesisInfiniteDatasource {
202
307
  * outstanding at once, and each settles as soon as the cache covers its range.
203
308
  * @internal
204
309
  */
310
+ /**
311
+ * Notes the CRITERIA_MATCH/ORDER_BY a block is being read with. A change means AG Grid has
312
+ * purged its cache and restarted from block 0: on DATASERVER the subscription is re-opened
313
+ * (those are DATA_LOGON parameters), and on both transports the host is told the rows it held
314
+ * are gone, so the rows read under the new filter or sort are reported as new.
315
+ * @internal
316
+ */
317
+ /**
318
+ * The sort a block is read with: the grid's own sort model when the user has sorted, and the
319
+ * `order-by` / `reverse` attributes otherwise.
320
+ * @remarks Without the fallback those attributes reached the comms datasource but were then
321
+ * dropped, because the logon builder removes ORDER_BY and REVERSE whenever the grid is
322
+ * unsorted. Sorting from the grid still wins, as it is the more specific instruction.
323
+ * @internal
324
+ */
325
+ resolveSortConfig(sortModel) {
326
+ const fromGrid = convertSortModelToOrderBy(sortModel, this.resourceIndexes, this.isRequestServer);
327
+ if (fromGrid.orderBy || !this.defaultOrderBy) {
328
+ return fromGrid;
329
+ }
330
+ return { orderBy: this.defaultOrderBy, reverse: this.defaultReverse };
331
+ }
332
+ noteRequestKey(criteria, sortConfig) {
333
+ var _a;
334
+ const key = JSON.stringify(Object.assign({ criteria }, sortConfig));
335
+ // Tracked separately from `subscriptionKey`, which a teardown clears: `reset()` tears the
336
+ // subscription down on every structural push, and a filter change landing between that and
337
+ // the next block read would otherwise look like the very first request - leaving the host
338
+ // holding every row read under the old filter.
339
+ if (this.rowCacheKey !== undefined && this.rowCacheKey !== key) {
340
+ // Whatever is in flight was read for the old filter or sort.
341
+ this.loadGeneration += 1;
342
+ this.ledger.clear();
343
+ (_a = this.onRowCacheResetFunc) === null || _a === void 0 ? void 0 : _a.call(this);
344
+ }
345
+ this.rowCacheKey = key;
346
+ // CRITERIA_MATCH and ORDER_BY are DATA_LOGON parameters, so a change needs a fresh
347
+ // subscription. A teardown has already left `subscriptionKey` undefined.
348
+ if (!this.isRequestServer &&
349
+ this.subscriptionKey !== undefined &&
350
+ this.subscriptionKey !== key) {
351
+ this.teardownDataserverSubscription();
352
+ }
353
+ this.subscriptionKey = key;
354
+ }
205
355
  getDataserverRows(params, criteria, sortConfig) {
206
356
  if (!this.createDataserverStreamFunc || !this.getMoreRowsFunc) {
207
357
  logger.error('Infinite DATASERVER paging requires a stream and a getMoreRows function');
208
358
  params.failCallback();
209
359
  return;
210
360
  }
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
361
  const startRow = Number.isFinite(Number(params.startRow)) ? Number(params.startRow) : 0;
220
362
  const endRow = Number.isFinite(Number(params.endRow))
221
363
  ? Number(params.endRow)
@@ -318,7 +460,7 @@ export class GenesisInfiniteDatasource {
318
460
  return;
319
461
  }
320
462
  this.sourceRef = (_b = message === null || message === void 0 ? void 0 : message.SOURCE_REF) !== null && _b !== void 0 ? _b : this.sourceRef;
321
- const filtered = (message === null || message === void 0 ? void 0 : message.ROW) ? dataServerResultFilter(message, this.rowId) : undefined;
463
+ const filtered = (message === null || message === void 0 ? void 0 : message.ROW) ? filterDataserverResult(message, this.rowId) : undefined;
322
464
  // A DATASERVER subscription is live by definition: batches answering the logon or a
323
465
  // MORE_ROWS carry the rows we asked for, and anything else is a change pushed by the server.
324
466
  // Replies only ever deliver view rows (INSERT operations), so a batch carrying a MODIFY or
@@ -380,7 +522,7 @@ export class GenesisInfiniteDatasource {
380
522
  deletes: (_k = (_j = result.deletes) === null || _j === void 0 ? void 0 : _j.length) !== null && _k !== void 0 ? _k : 0,
381
523
  });
382
524
  this.reset();
383
- (_l = this.onRowsInvalidatedFunc) === null || _l === void 0 ? void 0 : _l.call(this);
525
+ (_l = this.onRowsInvalidatedFunc) === null || _l === void 0 ? void 0 : _l.call(this, result);
384
526
  return;
385
527
  }
386
528
  if (modified) {
@@ -388,7 +530,7 @@ export class GenesisInfiniteDatasource {
388
530
  logger.debug('Infinite DATASERVER live update patched into the cache', {
389
531
  updates: (_o = (_m = result.updates) === null || _m === void 0 ? void 0 : _m.length) !== null && _o !== void 0 ? _o : 0,
390
532
  });
391
- (_p = this.onRowsUpdatedFunc) === null || _p === void 0 ? void 0 : _p.call(this);
533
+ (_p = this.onRowsUpdatedFunc) === null || _p === void 0 ? void 0 : _p.call(this, result);
392
534
  }
393
535
  }
394
536
  /**
@@ -506,6 +648,46 @@ export class GenesisInfiniteDatasource {
506
648
  lastRow,
507
649
  });
508
650
  block.success(rows, lastRow);
651
+ this.deliverRows(block.startRow, rows);
652
+ this.notifyDataAvailability(block.startRow, rows.length);
653
+ }
654
+ /**
655
+ * Raises the no-data / data-available hooks for the first block, as the server-side resource
656
+ * does. Later blocks say nothing: the question is whether the resource has any rows at all,
657
+ * and the answer cannot change once the first block has been served.
658
+ * @internal
659
+ */
660
+ notifyDataAvailability(startRow, rowCount) {
661
+ var _a, _b;
662
+ if (startRow !== 0 || this.detached) {
663
+ return;
664
+ }
665
+ if (rowCount === 0) {
666
+ (_a = this.onNoDataAvailableFunc) === null || _a === void 0 ? void 0 : _a.call(this);
667
+ }
668
+ else {
669
+ (_b = this.onDataAvailableFunc) === null || _b === void 0 ? void 0 : _b.call(this);
670
+ }
671
+ }
672
+ /**
673
+ * Reports rows handed to the grid for a block, then the rows a re-read of that block no longer
674
+ * carries and the rows whose block the grid evicted.
675
+ * @internal
676
+ */
677
+ deliverRows(startRow, rows) {
678
+ var _a, _b;
679
+ // A reply that resolves after the host dropped this instance belongs to a cache the grid no
680
+ // longer has; reporting it would tell consumers about rows the grid will never show.
681
+ if (this.detached) {
682
+ return;
683
+ }
684
+ const { withdrawnRowIds } = this.ledger.record(startRow, rows);
685
+ if (rows.length > 0) {
686
+ (_a = this.onRowsDeliveredFunc) === null || _a === void 0 ? void 0 : _a.call(this, rows);
687
+ }
688
+ if (withdrawnRowIds.length > 0) {
689
+ (_b = this.onRowsWithdrawnFunc) === null || _b === void 0 ? void 0 : _b.call(this, withdrawnRowIds);
690
+ }
509
691
  }
510
692
  /**
511
693
  * Warns when the view filled up before the data ran out.
@@ -566,10 +748,33 @@ export class GenesisInfiniteDatasource {
566
748
  this.moreRows = undefined;
567
749
  this.moreRowsInFlight = false;
568
750
  }
751
+ /**
752
+ * Drops the subscription and the cached rows.
753
+ * @remarks AG Grid calls this on the bean it then keeps using (`InfiniteRowModel.start()`
754
+ * re-applies the `datasource` grid option, and `setDatasource` destroys the previous one
755
+ * first), and it can land while a block request is in flight. So it must leave the datasource
756
+ * able to serve that reply and the next `getRows`: it drops the subscription, not the instance.
757
+ */
569
758
  destroy() {
570
759
  this.teardownDataserverSubscription();
760
+ this.ledger.clear();
571
761
  logger.debug('GenesisInfiniteDatasource destroyed');
572
762
  }
763
+ /**
764
+ * Drops this instance for good: the host is replacing it, so it must stop serving the grid and
765
+ * stop reporting rows, even though AG Grid still holds it as its `datasource` until the
766
+ * replacement is attached.
767
+ * @remarks Deliberately separate from `destroy()`, which AG Grid calls on a bean it then keeps
768
+ * using (`InfiniteRowModel.start()` re-applies the `datasource` grid option, and `setDatasource`
769
+ * destroys the previous one first).
770
+ */
771
+ detach() {
772
+ this.detached = true;
773
+ this.loadGeneration += 1;
774
+ this.rowCacheKey = undefined;
775
+ this.destroy();
776
+ logger.debug('GenesisInfiniteDatasource detached by its host');
777
+ }
573
778
  }
574
779
  /**
575
780
  * Max time a DATASERVER block may stay pending before the safety valve resolves it with the