@genesislcap/grid-pro 15.29.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.
- package/dist/custom-elements.json +1881 -1479
- package/dist/dts/datasource/base.datasource.d.ts +69 -8
- package/dist/dts/datasource/base.datasource.d.ts.map +1 -1
- package/dist/dts/datasource/base.types.d.ts +10 -0
- package/dist/dts/datasource/base.types.d.ts.map +1 -1
- package/dist/dts/datasource/dataserver-result.filter.d.ts +17 -0
- package/dist/dts/datasource/dataserver-result.filter.d.ts.map +1 -0
- package/dist/dts/datasource/delivered-block.ledger.d.ts +37 -0
- package/dist/dts/datasource/delivered-block.ledger.d.ts.map +1 -0
- package/dist/dts/datasource/infinite.datasource.d.ts +65 -1
- package/dist/dts/datasource/infinite.datasource.d.ts.map +1 -1
- package/dist/dts/datasource/infinite.resource.d.ts +136 -6
- package/dist/dts/datasource/infinite.resource.d.ts.map +1 -1
- package/dist/dts/datasource/server-side.datasource.d.ts +8 -4
- package/dist/dts/datasource/server-side.datasource.d.ts.map +1 -1
- package/dist/dts/datasource/server-side.resource-base.d.ts +23 -0
- package/dist/dts/datasource/server-side.resource-base.d.ts.map +1 -1
- package/dist/dts/datasource/server-side.resource-dataserver.d.ts +28 -0
- package/dist/dts/datasource/server-side.resource-dataserver.d.ts.map +1 -1
- package/dist/dts/datasource/server-side.resource-reqrep.d.ts +1 -1
- package/dist/dts/datasource/server-side.resource-reqrep.d.ts.map +1 -1
- package/dist/dts/grid-pro-beta.d.ts +6 -0
- package/dist/dts/grid-pro-beta.d.ts.map +1 -1
- package/dist/dts/grid-pro-genesis-datasource/datasource-events.types.d.ts +4 -0
- package/dist/dts/grid-pro-genesis-datasource/datasource-events.types.d.ts.map +1 -1
- package/dist/dts/grid-pro.d.ts.map +1 -1
- package/dist/dts/react.d.ts +11 -11
- package/dist/esm/datasource/base.datasource.js +182 -27
- package/dist/esm/datasource/dataserver-result.filter.js +28 -0
- package/dist/esm/datasource/delivered-block.ledger.js +52 -0
- package/dist/esm/datasource/infinite.datasource.js +131 -20
- package/dist/esm/datasource/infinite.resource.js +221 -16
- package/dist/esm/datasource/server-side.datasource.js +80 -29
- package/dist/esm/datasource/server-side.resource-base.js +39 -0
- package/dist/esm/datasource/server-side.resource-dataserver.js +110 -23
- package/dist/esm/datasource/server-side.resource-reqrep.js +25 -3
- package/dist/esm/grid-pro-beta.js +20 -2
- package/dist/esm/grid-pro.js +4 -1
- package/dist/grid-pro.api.json +35 -35
- package/dist/grid-pro.d.ts +290 -18
- package/dist/react.cjs +17 -17
- package/dist/react.mjs +16 -16
- package/package.json +13 -13
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks which row ids each block has handed to AG Grid, so the host can learn when a re-read of
|
|
3
|
+
* a block no longer returns a row (it left the grid).
|
|
4
|
+
* @remarks Plain class shared by the infinite and SSRM resources - AG Grid does not report that
|
|
5
|
+
* event.
|
|
6
|
+
*
|
|
7
|
+
* Block eviction is deliberately **not** mirrored here. AG Grid evicts by last access
|
|
8
|
+
* (`InfiniteCache.purgeBlocksIfNeeded` sorts on `lastAccessed` and never evicts a displayed or
|
|
9
|
+
* focused block; the SSRM `LazyCache` purges by distance from the viewport), and a datasource is
|
|
10
|
+
* told about neither the accesses nor the evictions. Any eviction model built from delivery order
|
|
11
|
+
* alone diverges from the grid's as soon as the user scrolls back, which would drop a visible
|
|
12
|
+
* row from the held set and silence the pushes that follow. So the held set means "rows delivered
|
|
13
|
+
* since the last cache reset": a superset of what the grid holds, which keeps every push for a
|
|
14
|
+
* possibly-visible row flowing and costs only a re-delivered row being reported as unchanged
|
|
15
|
+
* rather than as an `add`.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export class DeliveredBlockLedger {
|
|
19
|
+
constructor(rowId) {
|
|
20
|
+
this.rowId = rowId;
|
|
21
|
+
/** Row ids per block start row. */
|
|
22
|
+
this.blocks = new Map();
|
|
23
|
+
}
|
|
24
|
+
/** Records the rows handed to the grid for the block at `startRow`. */
|
|
25
|
+
record(startRow, rows) {
|
|
26
|
+
const ids = new Set();
|
|
27
|
+
rows.forEach((row) => {
|
|
28
|
+
const id = row === null || row === void 0 ? void 0 : row[this.rowId];
|
|
29
|
+
if (id !== undefined && id !== null) {
|
|
30
|
+
ids.add(id);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const previous = this.blocks.get(startRow);
|
|
34
|
+
this.blocks.set(startRow, ids);
|
|
35
|
+
const withdrawnRowIds = previous
|
|
36
|
+
? [...previous].filter((id) => !ids.has(id) && !this.isHeld(id))
|
|
37
|
+
: [];
|
|
38
|
+
return { withdrawnRowIds };
|
|
39
|
+
}
|
|
40
|
+
/** Forgets every block, for when the grid drops its whole cache (filter/sort change, destroy). */
|
|
41
|
+
clear() {
|
|
42
|
+
this.blocks.clear();
|
|
43
|
+
}
|
|
44
|
+
isHeld(id) {
|
|
45
|
+
for (const ids of this.blocks.values()) {
|
|
46
|
+
if (ids.has(id)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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) ?
|
|
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
|