Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,11 @@
"engines": {
"pnpm": "10.30.1",
"node": ">=22.18"
},
"dependencies": {},
"pnpm": {
"overrides": {
"@base-ui/utils": "https://pkg.pr.new/mui/base-ui/@base-ui/utils@0a56f30"
}
}
}
1 change: 1 addition & 0 deletions packages/x-data-grid-pro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"dependencies": {
"@babel/runtime": "catalog:",
"@base-ui/utils": "^0.2.3",
"@mui/utils": "catalog:",
"@mui/x-data-grid": "workspace:*",
"@mui/x-internals": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import { warnOnce } from '@mui/x-internals/warning';
import type { GridPrivateApiPro } from '../../../models/gridApiPro';
import type { DataGridProProcessedProps } from '../../../models/dataGridProProps';
import { NestedDataManager, RequestStatus, getGroupKeys } from './utils';
import { GridRequestQueue, getGroupKeys } from './utils';
import type {
GridDataSourceApiBasePro,
GridDataSourceApiPro,
Expand All @@ -52,20 +52,20 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(
options: GridDataSourceBaseOptions = {},
) => {
const groupsToAutoFetch = useGridSelector(apiRef, gridRowGroupsToFetchSelector);
const nestedDataManager = useLazyRef<NestedDataManager, void>(
() => new NestedDataManager(apiRef),
const requestQueue = useLazyRef<GridRequestQueue, void>(
() => new GridRequestQueue(apiRef),
).current;
const scheduledGroups = React.useRef<number>(0);

const clearDataSourceState = React.useCallback(() => {
nestedDataManager.clear();
requestQueue.clear();
scheduledGroups.current = 0;
const dataSourceState = apiRef.current.state.dataSource;
if (dataSourceState !== INITIAL_STATE) {
apiRef.current.resetDataSourceState();
}
return null;
}, [apiRef, nestedDataManager]);
}, [apiRef, requestQueue]);

const handleEditRow = React.useCallback(
(params: GridUpdateRowParams, updatedRow: GridRowModel) => {
Expand All @@ -88,7 +88,7 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(
cacheChunkManager,
cache,
} = useGridDataSourceBase(apiRef, props, {
fetchRowChildren: nestedDataManager.queue,
fetchRowChildren: requestQueue.queue,
clearDataSourceState,
handleEditRow,
...options,
Expand Down Expand Up @@ -155,18 +155,18 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(
{},
) as Partial<GridGetRowsParamsPro & { groupFields: string[] }>;
if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) {
nestedDataManager.clearPendingRequest(id);
requestQueue.clearPendingRequest(id);
return;
}
const getRows = props.dataSource?.getRows;
if (!getRows) {
nestedDataManager.clearPendingRequest(id);
requestQueue.clearPendingRequest(id);
return;
}

const rowNode = apiRef.current.getRowNode<GridDataSourceGroupNode>(id);
if (!rowNode) {
nestedDataManager.clearPendingRequest(id);
requestQueue.clearPendingRequest(id);
return;
}

Expand All @@ -184,7 +184,7 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(

if (cachedData !== undefined) {
const rows = cachedData.rows;
nestedDataManager.setRequestSettled(id);
requestQueue.setRequestSettled(id);
replaceGroupRows(id, rowNode.path, rows);
if (cachedData.rowCount !== undefined) {
apiRef.current.setRowCount(cachedData.rowCount);
Expand All @@ -203,14 +203,14 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(
const getRowsResponse = await getRows(fetchParams);
if (!apiRef.current.getRowNode(id)) {
// The row has been removed from the grid
nestedDataManager.clearPendingRequest(id);
requestQueue.clearPendingRequest(id);
return;
}
if (nestedDataManager.getRequestStatus(id) === RequestStatus.UNKNOWN) {
if (requestQueue.getRequestStatus(id) === 'unknown') {
apiRef.current.dataSource.setChildrenLoading(id, false);
return;
}
nestedDataManager.setRequestSettled(id);
requestQueue.setRequestSettled(id);

const cacheResponses = cacheChunkManager.splitResponse(fetchParams, getRowsResponse);
cacheResponses.forEach((response, key) => {
Expand Down Expand Up @@ -245,11 +245,11 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(
}
} finally {
apiRef.current.dataSource.setChildrenLoading(id, false);
nestedDataManager.setRequestSettled(id);
requestQueue.setRequestSettled(id);
}
},
[
nestedDataManager,
requestQueue,
cacheChunkManager,
cache,
onDataSourceErrorProp,
Expand Down Expand Up @@ -422,10 +422,10 @@ export const useGridDataSourceBasePro = <Api extends GridPrivateApiPro>(
scheduledGroups.current < groupsToAutoFetch.length
) {
const groupsToSchedule = groupsToAutoFetch.slice(scheduledGroups.current);
nestedDataManager.queue(groupsToSchedule);
requestQueue.queue(groupsToSchedule);
scheduledGroups.current = groupsToAutoFetch.length;
}
}, [apiRef, nestedDataManager, groupsToAutoFetch]);
}, [apiRef, requestQueue, groupsToAutoFetch]);

return {
api: { public: dataSourceApi, private: dataSourcePrivateApi },
Expand Down
122 changes: 29 additions & 93 deletions packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,117 +6,53 @@ import {
type GridRowId,
type GridRowTreeConfig,
} from '@mui/x-data-grid';
import { RequestQueue, type RequestStatus } from '@base-ui/utils/RequestQueue';
import type { GridPrivateApiPro } from '../../../models';

const MAX_CONCURRENT_REQUESTS = Infinity;

export enum RequestStatus {
QUEUED,
PENDING,
SETTLED,
UNKNOWN,
}

/**
* Fetches row children from the server with option to limit the number of concurrent requests
* Determines the status of a request based on the enum `RequestStatus`
* Uses `GridRowId` to uniquely identify a request
* Thin wrapper around `RequestQueue` that ties request lifecycle to
* the Data Grid's loading-state bookkeeping.
*/
export class NestedDataManager {
private pendingRequests: Set<GridRowId> = new Set();

private queuedRequests: Set<GridRowId> = new Set();

private settledRequests: Set<GridRowId> = new Set();
export class GridRequestQueue {
private requestQueue: RequestQueue<GridRowId>;

private api: GridPrivateApiPro;

private maxConcurrentRequests: number;

constructor(
privateApiRef: RefObject<GridPrivateApiPro>,
maxConcurrentRequests = MAX_CONCURRENT_REQUESTS,
) {
this.api = privateApiRef.current;
this.maxConcurrentRequests = maxConcurrentRequests;
constructor(apiRef: RefObject<GridPrivateApiPro>) {
this.api = apiRef.current;
this.requestQueue = new RequestQueue<GridRowId>({
fetchFn: (id) => {
this.api.fetchRowChildren(id);
return Promise.resolve();
},
});
}

private processQueue = async () => {
if (this.queuedRequests.size === 0 || this.pendingRequests.size >= this.maxConcurrentRequests) {
return;
}
const loopLength = Math.min(
this.maxConcurrentRequests - this.pendingRequests.size,
this.queuedRequests.size,
);
if (loopLength === 0) {
return;
}
const fetchQueue = Array.from(this.queuedRequests);

for (let i = 0; i < loopLength; i += 1) {
const id = fetchQueue[i];
this.queuedRequests.delete(id);
this.pendingRequests.add(id);
this.api.fetchRowChildren(id);
}
};

public queue = async (ids: GridRowId[], options: { showChildrenLoading?: boolean } = {}) => {
const { showChildrenLoading = true } = options;
const loadingIds: Record<GridRowId, boolean> = {};
ids.forEach((id) => {
this.queuedRequests.add(id);
if (showChildrenLoading) {
loadingIds[id] = true;
}
});
if (showChildrenLoading) {
this.api.setState((state) => ({
...state,
dataSource: {
...state.dataSource,
loading: {
...state.dataSource.loading,
...loadingIds,
},
public queue = (ids: GridRowId[]) => {
const loadingIds = Object.fromEntries(ids.map((id) => [id, true]));
this.api.setState((state) => ({
...state,
dataSource: {
...state.dataSource,
loading: {
...state.dataSource.loading,
...loadingIds,
},
}));
}
this.processQueue();
},
}));
return this.requestQueue.queue(ids);
};

public setRequestSettled = (id: GridRowId) => {
this.pendingRequests.delete(id);
this.settledRequests.add(id);
this.processQueue();
};
public setRequestSettled = (id: GridRowId) => this.requestQueue.setRequestSettled(id);

public clear = () => {
this.queuedRequests.clear();
Array.from(this.pendingRequests).forEach((id) => this.clearPendingRequest(id));
};
public clear = () => this.requestQueue.clear();

public clearPendingRequest = (id: GridRowId) => {
this.api.dataSource.setChildrenLoading(id, false);
this.pendingRequests.delete(id);
this.processQueue();
};

public getRequestStatus = (id: GridRowId) => {
if (this.pendingRequests.has(id)) {
return RequestStatus.PENDING;
}
if (this.queuedRequests.has(id)) {
return RequestStatus.QUEUED;
}
if (this.settledRequests.has(id)) {
return RequestStatus.SETTLED;
}
return RequestStatus.UNKNOWN;
return this.requestQueue.clearPendingRequest(id);
};

public getActiveRequestsCount = () => this.pendingRequests.size + this.queuedRequests.size;
public getRequestStatus = (id: GridRowId): RequestStatus => this.requestQueue.getRequestStatus(id);
}

export const getGroupKeys = (tree: GridRowTreeConfig, rowId: GridRowId) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
buildEventsState,
SchedulerEventParameters,
} from '@mui/x-scheduler-headless/internals';
import { DebouncedRequestQueue } from '@base-ui/utils/DebouncedRequestQueue';
import { SchedulerDataSourceCacheDefault } from '../utils/cache';
import { SchedulerDataManager } from '../utils/queue';
import { type DateRange, getDateRangeKey } from '../utils/queue';

export class SchedulerLazyLoadingPlugin<
TEvent extends object,
Expand All @@ -16,18 +17,21 @@ export class SchedulerLazyLoadingPlugin<
> {
private store: SchedulerStore<TEvent, any, State, Parameters>;

private dataManager: SchedulerDataManager | null = null;
private requestQueue: DebouncedRequestQueue<DateRange> | null = null;
private cache: SchedulerDataSourceCacheDefault<TEvent> | null = null;

constructor(store: SchedulerStore<TEvent, any, State, Parameters>) {
this.store = store;

if (this.store.parameters.dataSource) {
this.cache = new SchedulerDataSourceCacheDefault<TEvent>({ ttl: 300_000 });
this.dataManager = new SchedulerDataManager(
this.store.state.adapter,
this.loadEventsFromDataSource,
);
this.requestQueue = new DebouncedRequestQueue<DateRange>({
fetchFn: (range) => this.loadEventsFromDataSource(range),
maxConcurrentRequests: 3,
getKeyId: (range) => getDateRangeKey(this.store.state.adapter, range),
debounceMs: 150,
maxQueuedRequests: 3,
});

// Subscribe to events updated event to sync cache
this.store.subscribeEvent('eventsUpdated', this.handleEventsUpdated);
Expand All @@ -41,7 +45,7 @@ export class SchedulerLazyLoadingPlugin<
},
immediate = false,
) => {
if (this.dataManager) {
if (this.requestQueue) {
const { adapter } = this.store.state;

// Set loading state immediately (before the debounce delay)
Expand All @@ -57,9 +61,9 @@ export class SchedulerLazyLoadingPlugin<
}

if (immediate) {
await this.dataManager.queueImmediate([range]);
await this.requestQueue.queueImmediate([range]);
} else {
await this.dataManager.queue([range]);
await this.requestQueue.queue([range]);
}
}
};
Expand All @@ -74,7 +78,7 @@ export class SchedulerLazyLoadingPlugin<
const { dataSource } = this.store.parameters;
const { adapter, displayTimezone } = this.store.state;

if (!dataSource || !this.cache || !this.dataManager) {
if (!dataSource || !this.cache || !this.requestQueue) {
return;
}
if (
Expand All @@ -96,7 +100,7 @@ export class SchedulerLazyLoadingPlugin<
isLoading: false,
});

await this.dataManager.setRequestSettled(range);
await this.requestQueue.setRequestSettled(range);

return;
}
Expand All @@ -119,14 +123,14 @@ export class SchedulerLazyLoadingPlugin<
errors: [],
});
// Mark request as settled
await this.dataManager.setRequestSettled(range);
await this.requestQueue.setRequestSettled(range);
} catch (error) {
this.store.set('errors', [error]);
await this.dataManager.setRequestSettled(range);
await this.requestQueue.setRequestSettled(range);
} finally {
// Unset loading state
this.store.set('isLoading', false);
await this.dataManager.setRequestSettled(range);
await this.requestQueue.setRequestSettled(range);
}
};

Expand Down
Loading
Loading