Skip to content

Commit bf60f23

Browse files
authored
T1311885 - TreeList – TypeError: Cannot read properties of undefined ('concat') when search and interval functions are called simultaneously (#31990)
1 parent 33e6d4c commit bf60f23

File tree

4 files changed

+186
-12
lines changed

4 files changed

+186
-12
lines changed
File renamed without changes.

packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default class DataSourceAdapter extends modules.Controller {
5656

5757
private _isRefreshed: any;
5858

59-
private _lastOperationId: any;
59+
protected _lastOperationId: any;
6060

6161
private _operationTypes: any;
6262

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, jest, test,
3+
} from '@jest/globals';
4+
import { Deferred } from '@js/core/utils/deferred';
5+
import type { Store } from '@js/data';
6+
import CustomStore from '@js/data/custom_store';
7+
import DataSource from '@js/data/data_source';
8+
9+
import { DataSourceAdapterTreeList } from './m_data_source_adapter';
10+
11+
describe('TreeList DataSourceAdapter - T1311885 Race Condition', () => {
12+
let dataSourceAdapter: DataSourceAdapterTreeList;
13+
let mockStore: Store;
14+
let loadCalls: { filter: any; deferred: any; type?: string }[];
15+
const parentData = [
16+
{ Task_ID: 1, Task_Parent_ID: 0, Task_Subject: 'Parent 1' },
17+
{ Task_ID: 2, Task_Parent_ID: 0, Task_Subject: 'Parent 2' },
18+
];
19+
20+
const childData = [
21+
{ Task_ID: 10, Task_Parent_ID: 1, Task_Subject: 'Child 1' },
22+
{ Task_ID: 20, Task_Parent_ID: 2, Task_Subject: 'Child 2' },
23+
];
24+
25+
const OPERATION_ID = {
26+
FIRST: 1,
27+
SECOND: 2,
28+
};
29+
30+
beforeEach(() => {
31+
loadCalls = [];
32+
33+
mockStore = new CustomStore({
34+
key: 'Task_ID',
35+
load: (options: any) => {
36+
// @ts-expect-error
37+
const deferred = new Deferred();
38+
loadCalls.push({ filter: options?.filter, deferred });
39+
return deferred.promise();
40+
},
41+
});
42+
43+
const dataSource = new DataSource({
44+
store: mockStore,
45+
reshapeOnPush: true,
46+
});
47+
48+
const mockComponent = {
49+
option: jest.fn((key: string) => {
50+
const options: any = {
51+
remoteOperations: { filtering: true, sorting: true },
52+
parentIdExpr: 'Task_Parent_ID',
53+
hasItemsExpr: 'Has_Items',
54+
filterMode: 'fullBranch',
55+
expandedRowKeys: [],
56+
dataStructure: 'plain',
57+
rootValue: 0,
58+
};
59+
return options[key];
60+
}),
61+
_createActionByOption: jest.fn(() => jest.fn()),
62+
on: jest.fn(() => mockComponent),
63+
off: jest.fn(() => mockComponent),
64+
_eventsStrategy: {
65+
on: jest.fn(),
66+
off: jest.fn(),
67+
fireEvent: jest.fn(),
68+
hasEvent: jest.fn(() => false),
69+
},
70+
} as any;
71+
72+
dataSourceAdapter = new DataSourceAdapterTreeList(mockComponent);
73+
dataSourceAdapter.init(dataSource, { remoteOperations: { filtering: true } });
74+
75+
(dataSourceAdapter as any)._loadDataSource = jest.fn((options: any) => {
76+
// @ts-expect-error
77+
const deferred = new Deferred();
78+
79+
loadCalls.push({
80+
filter: options?.filter,
81+
deferred,
82+
type: 'dataSource',
83+
});
84+
85+
return deferred.promise();
86+
});
87+
});
88+
89+
afterEach(() => {
90+
jest.clearAllMocks();
91+
jest.restoreAllMocks();
92+
loadCalls = [];
93+
(dataSourceAdapter as any)._loadDataSource = undefined;
94+
(dataSourceAdapter as any).loadFromStore = undefined;
95+
mockStore = undefined as any;
96+
dataSourceAdapter = undefined as any;
97+
});
98+
99+
test('T1311885 - _loadParentsOrChildren should NOT throw concat error when _cachedStoreData is cleared', async () => {
100+
let firstLoadDeferred: any = null;
101+
let errorMessage = '';
102+
103+
const unhandledRejectionHandler = (reason: any) => {
104+
errorMessage = reason?.message || String(reason);
105+
};
106+
process.on('unhandledRejection', unhandledRejectionHandler);
107+
108+
(dataSourceAdapter as any)._cachedStoreData = parentData;
109+
(dataSourceAdapter as any)._dataSource = {
110+
store: jest.fn(() => mockStore),
111+
cancel: jest.fn(),
112+
};
113+
(dataSourceAdapter as any)._lastOperationId = OPERATION_ID.FIRST;
114+
115+
const options = {
116+
remoteOperations: { filtering: true },
117+
storeLoadOptions: { sort: null },
118+
loadOptions: { sort: null },
119+
operationId: OPERATION_ID.FIRST,
120+
};
121+
122+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
123+
(dataSourceAdapter as any).loadFromStore = jest.fn((loadOptions, store) => {
124+
// @ts-expect-error
125+
const deferred = new Deferred();
126+
127+
if (!firstLoadDeferred) {
128+
firstLoadDeferred = deferred;
129+
}
130+
131+
return deferred.promise();
132+
});
133+
134+
(dataSourceAdapter as any)._loadParentsOrChildren(
135+
childData,
136+
options,
137+
);
138+
139+
expect((dataSourceAdapter as any).loadFromStore).toHaveBeenCalledTimes(1);
140+
expect(firstLoadDeferred).toBeDefined();
141+
142+
(dataSourceAdapter as any)._cachedStoreData = undefined;
143+
(dataSourceAdapter as any)._lastOperationId = OPERATION_ID.SECOND;
144+
145+
firstLoadDeferred.resolve(parentData);
146+
await Promise.resolve();
147+
148+
process.off('unhandledRejection', unhandledRejectionHandler);
149+
150+
expect(errorMessage).toBe('');
151+
expect(errorMessage).not.toMatch(/concat|Cannot read properties of undefined/i);
152+
});
153+
});

packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const applySorting = (data: any[], sort: any): any => queryByOptions(
4343
},
4444
).toArray();
4545

46-
class DataSourceAdapterTreeList extends DataSourceAdapter {
46+
export class DataSourceAdapterTreeList extends DataSourceAdapter {
4747
private _indexByKey: any;
4848

4949
private _keyGetter: any;
@@ -377,7 +377,21 @@ class DataSourceAdapterTreeList extends DataSourceAdapter {
377377
};
378378
}
379379

380+
private _isOperationIdOutdated(operationId) {
381+
return operationId !== undefined
382+
&& this._lastOperationId !== undefined
383+
&& operationId !== this._lastOperationId;
384+
}
385+
380386
private _loadParentsOrChildren(data, options, needChildren?) {
387+
if (this._isOperationIdOutdated(options.operationId)) {
388+
this._dataSource.cancel(options.operationId);
389+
// @ts-expect-error
390+
const rejectedDeferred = new Deferred();
391+
rejectedDeferred.reject();
392+
return rejectedDeferred;
393+
}
394+
381395
let filter;
382396
let needLocalFiltering;
383397
const { keys, keyMap } = this._generateInfoToLoad(data, needChildren);
@@ -434,17 +448,24 @@ class DataSourceAdapterTreeList extends DataSourceAdapter {
434448

435449
const store = options.fullData ? new ArrayStore(options.fullData) : this._dataSource.store();
436450

437-
this.loadFromStore(loadOptions, store).done((loadedData) => {
438-
if (loadedData.length) {
439-
if (needLocalFiltering) {
440-
// @ts-expect-error
441-
loadedData = query(loadedData).filter(filter).toArray();
451+
this.loadFromStore(loadOptions, store)
452+
.done((loadedData) => {
453+
if (this._isOperationIdOutdated(options.operationId)) {
454+
d.reject();
455+
return;
442456
}
443-
this._loadParentsOrChildren(concatLoadedData(loadedData), options, needChildren).done(d.resolve).fail(d.reject);
444-
} else {
445-
d.resolve(data);
446-
}
447-
}).fail(d.reject);
457+
458+
if (loadedData.length) {
459+
if (needLocalFiltering) {
460+
// @ts-expect-error
461+
loadedData = query(loadedData).filter(filter).toArray();
462+
}
463+
this._loadParentsOrChildren(concatLoadedData(loadedData), options, needChildren).done(d.resolve).fail(d.reject);
464+
} else {
465+
d.resolve(data);
466+
}
467+
})
468+
.fail(d.reject);
448469

449470
return d;
450471
}

0 commit comments

Comments
 (0)